Skip to content
This repository was archived by the owner on Sep 9, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 4 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
57 changes: 47 additions & 10 deletions docarray/index/backends/in_memory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import sys
from collections import defaultdict
from dataclasses import dataclass, field
from typing import (
Expand All @@ -22,10 +23,7 @@
from docarray.array.any_array import AnyDocArray
from docarray.helper import _shallow_copy_doc
from docarray.index.abstract import BaseDocIndex, _raise_not_supported
from docarray.index.backends.helper import (
_collect_query_args,
_execute_find_and_filter_query,
)
from docarray.index.backends.helper import _collect_query_args
from docarray.typing import AnyTensor, NdArray
from docarray.typing.tensor.abstract_tensor import AbstractTensor
from docarray.utils._internal._typing import safe_issubclass
Expand Down Expand Up @@ -293,12 +291,51 @@ def execute_query(self, query: List[Tuple[str, Dict]], *args, **kwargs) -> Any:
raise ValueError(
f'args and kwargs not supported for `execute_query` on {type(self)}'
)
find_res = _execute_find_and_filter_query(
doc_index=self,
query=query,
reverse_order=True,
)
return find_res
return self._find_and_filter(query)

def _find_and_filter(self, query: List[Tuple[str, Dict]]) -> FindResult:
"""
Executes a hybrid search on documents based on the provided query.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change docstring as well pls

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep this term, we (and not only us) use it in docs and even though most ppl think of bm25 when talking about hybrid search, this is not wrong

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use find and filter which is what it is


The function performs search operations such as 'find' and 'filter' in the order
they appear in the query. The 'find' operation performs a vector similarity search.
The 'filter' operation filters out documents based on a filter query.
The documents are finally sorted based on their scores.

:param query: The query to execute.
:return: A tuple of retrieved documents and their scores.
"""
out_docs = self._docs
doc_to_score: Dict[BaseDoc, Any] = {}
limit = sys.maxsize

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't u just do limit=10 here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored limit logic, uses whatever is passed, and if nothing's passed goes with len(out_docs)

for op, op_kwargs in query:
limit = min(limit, op_kwargs['limit']) if op_kwargs.get('limit') else limit
if op == 'find':
out_docs, scores = find(
index=out_docs,
query=op_kwargs['query'],
search_field=op_kwargs['search_field'],
limit=len(out_docs),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think limit should be the limit obtained or the ln(out_docs) if no limit present

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, I think I made it too complicated

metric=self._column_infos[op_kwargs['search_field']].config[
'space'
],
)
doc_to_score.update(zip(out_docs.id, scores))
elif op == 'filter':
out_docs = filter_docs(out_docs, op_kwargs['filter_query'])
else:
raise ValueError(f'Query operation is not supported: {op}')

# if limit was not provided, use the default
if limit == sys.maxsize:
limit = 10

out_docs = out_docs[:limit]
scores_and_docs = zip([doc_to_score[doc.id] for doc in out_docs], out_docs)
sorted_lists = sorted(scores_and_docs, reverse=True)
out_scores, out_docs = zip(*sorted_lists)

return FindResult(documents=out_docs, scores=out_scores)

def find(
self,
Expand Down
56 changes: 42 additions & 14 deletions tests/index/in_memory/test_in_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
tf_available = is_tf_available()
if tf_available:
import tensorflow as tf
from docarray.typing import TensorFlowTensor


class SchemaDoc(BaseDoc):
Expand Down Expand Up @@ -165,37 +164,66 @@ def test_with_text_doc_torch():
assert len(r) == 5


def test_concatenated_queries(doc_index):
query = SchemaDoc(text='query', price=0, tensor=np.ones(10))

def test_query_builder_pre_filtering(doc_index):
q = (
doc_index.build_query()
.find(query=query, search_field='tensor', limit=5)
.filter(filter_query={'price': {'$neq': 5}})
.filter(filter_query={'price': {'$lte': 3}})
.find(query=np.ones(10), search_field='tensor', limit=5)
.build()
)

docs, scores = doc_index.execute_query(q)

assert len(docs) == 4
for doc in docs:
assert doc.price <= 3


def test_query_builder_post_filtering(doc_index):
q = (
doc_index.build_query()
.find(query=np.ones(10), search_field='tensor', limit=5)
.filter(filter_query={'price': {'$gt': 7}})
.build()
)

docs, scores = doc_index.execute_query(q)

assert len(docs) == 2
for doc in docs:
assert doc.price > 7

@pytest.mark.parametrize(
'find_limit, filter_limit, expected_docs', [(10, 3, 3), (5, None, 1)]
)
def test_query_builder_limits(doc_index, find_limit, filter_limit, expected_docs):
query = SchemaDoc(text='query', price=3, tensor=np.array([3] * 10))

def test_query_builder_pre_post_filtering(doc_index):
q = (
doc_index.build_query()
.find(query=query, search_field='tensor', limit=find_limit)
.filter(filter_query={'price': {'$lte': 5}}, limit=filter_limit)
.filter(filter_query={'price': {'$lte': 3}})
.find(query=np.ones(10), search_field='tensor')
.filter(filter_query={'text': {'$eq': 'hello 1'}})
.build()
)

docs, scores = doc_index.execute_query(q)

assert len(docs) == expected_docs
assert len(docs) == 1
assert docs[0].text == 'hello 1' and docs[0].price <= 3


def test_find_and_filter(doc_index):
q = (
doc_index.build_query()
.filter(filter_query={'price': {'$lt': 3}})
.find(query=np.ones(10), search_field='tensor')
.filter(filter_query={'text': {'$neq': 'hello 2'}})
.build()
)

docs, scores = doc_index._find_and_filter(q)

assert len(docs) == 2
for doc in docs:
assert doc.text in ['hello 1', 'hello 0']
assert doc.price < 3


def test_filter(doc_index):
Expand Down