Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.geaflow.ai.index.ann;

import java.util.ArrayList;
import java.util.List;
import org.apache.geaflow.ai.index.vector.IVector;

/**
* Exact linear scan over the added entries — the default {@link NearestNeighborIndex} adapter for
* Phase 1 and the reference implementation an ANN adapter is measured against.
*
* <p>Every search scores every entry through {@code entryVector.match(query)} and sorts by the
* interface contract: score descending, ties broken by insertion order. The sort is stable over
* the insertion sequence, which is what makes equal-scored neighbors come out in the order they
* were added. Results are a fresh list; mutating it does not affect the index.
*
* <p>This adapter makes no performance claim: it is the correctness baseline. Swap-in of an ANN
* implementation is a change behind the same interface, not an optimization of this class.
*
* @param <T> id type of the indexed entries
*/
public class LinearNearestNeighborIndex<T> implements NearestNeighborIndex<T> {

private final List<Entry<T>> entries = new ArrayList<>();

@Override
public void add(T id, IVector vector) {
if (id == null || vector == null) {
throw new IllegalArgumentException("id and vector must not be null");
}
entries.add(new Entry<>(id, vector));
}

@Override
public List<Neighbor<T>> search(IVector query, int topK) {
if (query == null) {
throw new IllegalArgumentException("query must not be null");
}
if (topK <= 0) {
throw new IllegalArgumentException("topK must be positive, got " + topK);
}
double[] scores = new double[entries.size()];
Integer[] order = new Integer[entries.size()];
for (int i = 0; i < entries.size(); i++) {
scores[i] = entries.get(i).vector.match(query);
order[i] = i;
}
// Tie-break key: the insertion ordinal travels alongside the score so equal-scored
// neighbors keep the order they were added in, independent of sort stability.
java.util.Arrays.sort(order, (a, b) -> {
int byScore = Double.compare(scores[b], scores[a]);
return byScore != 0 ? byScore : Integer.compare(a, b);
});
List<Neighbor<T>> result = new ArrayList<>(Math.min(topK, order.length));
for (int i = 0; i < Math.min(topK, order.length); i++) {
Entry<T> entry = entries.get(order[i]);
result.add(new Neighbor<>(entry.id, scores[order[i]]));
}
return result;
}

@Override
public int size() {
return entries.size();
}

private static final class Entry<T> {
private final T id;
private final IVector vector;

private Entry(T id, IVector vector) {
this.id = id;
this.vector = vector;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.geaflow.ai.index.ann;

import java.util.List;
import org.apache.geaflow.ai.index.vector.IVector;

/**
* Nearest-neighbor search over {@link IVector} entries, keyed by an opaque id.
*
* <p>Phase 1 ships this contract with the exact {@link LinearNearestNeighborIndex} only — vector
* search today is a linear TopN over the candidate set, and this interface exists so an ANN
* implementation (HNSW and friends) can slot in behind the same calls later without touching the
* retrieval path. No performance claim is made or implied by implementing this interface.
*
* <p><b>Contract.</b> Implementations are not required to be thread-safe. {@link #add} appends an
* entry; adding the same id twice keeps both entries (append-only semantics, like most ANN
* structures). {@link #search}:
*
* <ul>
* <li>returns at most {@code topK} neighbors, ordered by descending {@link Neighbor#getScore};</li>
* <li>breaks score ties by <b>insertion order</b> — of equally-scored entries, the one added
* first ranks first — so two runs over the same data produce the same ranking;</li>
* <li>must throw {@link IllegalArgumentException} for {@code topK <= 0};</li>
* <li>returns an empty list when the index holds no entries.</li>
* </ul>
*
* <p>Score direction is fixed as {@code entryVector.match(query)} — never the other way around —
* because some {@link IVector} implementations are not symmetric in their arguments.
*
* <p>An exact adapter must score every entry with {@link IVector#match} and honor the full
* ordering above. An approximate adapter may miss neighbors and coarsen scores, but must still
* respect the result shape: at most {@code topK} entries, no id appearing twice unless it was
* added twice, and a non-increasing score order within the returned page.
*
* @param <T> id type of the indexed entries
*/
public interface NearestNeighborIndex<T> {

/**
* Appends one entry to the index.
*
* @param id caller-defined id returned in search results
* @param vector representation the entry is searched by
*/
void add(T id, IVector vector);

/**
* Returns up to {@code topK} nearest entries for the query, ordered by the contract above.
*
* @param query vector to search against
* @param topK maximum number of results, must be positive
* @return ranked neighbors, highest score first
*/
List<Neighbor<T>> search(IVector query, int topK);

/** Number of entries currently held (an id added twice counts twice). */
int size();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.geaflow.ai.index.ann;

import java.util.Objects;

/**
* One search result of a {@link NearestNeighborIndex}: the caller-supplied id plus its similarity
* score against the query, as produced by {@link org.apache.geaflow.ai.index.vector.IVector#match}.
*/
public final class Neighbor<T> {

private final T id;
private final double score;

public Neighbor(T id, double score) {
this.id = Objects.requireNonNull(id);
this.score = score;
}

public T getId() {
return id;
}

public double getScore() {
return score;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Neighbor<?> other = (Neighbor<?>) o;
return Double.compare(score, other.score) == 0 && id.equals(other.id);
}

@Override
public int hashCode() {
return Objects.hash(id, score);
}

@Override
public String toString() {
return "Neighbor{" + "id=" + id + ", score=" + score + '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.geaflow.ai.index.ann;

import org.apache.geaflow.ai.index.vector.EmbeddingVector;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
* The linear adapter runs the full {@link NearestNeighborIndexContractTest} suite — including the
* exactness tests — plus its own argument validation. This is the adapter a future ANN
* implementation is measured against: same tests, same orderings, no approximations.
*/
public class LinearNearestNeighborIndexTest extends NearestNeighborIndexContractTest {

@Override
protected NearestNeighborIndex<String> newIndex() {
return new LinearNearestNeighborIndex<>();
}

@Test
public void rejectsNullArguments() {
LinearNearestNeighborIndex<String> index = new LinearNearestNeighborIndex<>();
Assertions.assertThrows(IllegalArgumentException.class,
() -> index.add(null, new EmbeddingVector(new double[]{1, 0})));
Assertions.assertThrows(IllegalArgumentException.class,
() -> index.add("a", null));
Assertions.assertThrows(IllegalArgumentException.class,
() -> index.search(null, 1));
}

/** The returned page is a fresh list: caller-side mutation cannot corrupt the index. */
@Test
public void returnedPageIsIndependentOfTheIndex() {
LinearNearestNeighborIndex<String> index = new LinearNearestNeighborIndex<>();
index.add("a", new EmbeddingVector(new double[]{1, 0}));
index.add("b", new EmbeddingVector(new double[]{0, 1}));
index.search(new EmbeddingVector(new double[]{1, 0}), 2).clear();
Assertions.assertEquals(2, index.search(new EmbeddingVector(new double[]{1, 0}), 2).size());
}
}
Loading
Loading