Skip to content

Commit cac4dfd

Browse files
authored
Merge pull request #12 from FlowLLM-AI/vector_store_update
feat(vector): add list filter support for metadata queries
2 parents 12e2bb3 + 0d2f625 commit cac4dfd

9 files changed

Lines changed: 64 additions & 33 deletions

File tree

flowllm/core/enumeration/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
"""Core enumeration module."""
22

33
from .chunk_enum import ChunkEnum
4-
from .content_block_type import ContentBlockType
54
from .http_enum import HttpEnum
65
from .registry_enum import RegistryEnum
76
from .role import Role
87

98
__all__ = [
109
"ChunkEnum",
11-
"ContentBlockType",
1210
"HttpEnum",
1311
"RegistryEnum",
1412
"Role",

flowllm/core/enumeration/content_block_type.py

Lines changed: 0 additions & 12 deletions
This file was deleted.

flowllm/core/schema/message.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pydantic import BaseModel, ConfigDict, Field, model_validator
77

88
from .tool_call import ToolCall
9-
from ..enumeration import Role, ContentBlockType
9+
from ..enumeration import Role
1010

1111

1212
class ContentBlock(BaseModel):
@@ -35,7 +35,7 @@ class ContentBlock(BaseModel):
3535

3636
model_config = ConfigDict(extra="allow")
3737

38-
type: ContentBlockType = Field(default="")
38+
type: str = Field(default="")
3939
content: str | dict | list = Field(default="")
4040

4141
@model_validator(mode="before")
@@ -51,8 +51,8 @@ def init_block(cls, data: dict):
5151
def simple_dump(self) -> dict:
5252
"""Convert ContentBlock to a simple dictionary format."""
5353
result = {
54-
"type": self.type.value,
55-
self.type.value: self.content,
54+
"type": self.type,
55+
self.type: self.content,
5656
**self.model_extra,
5757
}
5858

@@ -95,11 +95,6 @@ def simple_dump(self, add_reasoning: bool = True) -> dict:
9595

9696
return result
9797

98-
@property
99-
def string_buffer(self) -> str:
100-
"""Get a string representation of the message with role and content."""
101-
return f"{self.role.value}: {self.dump_content()}"
102-
10398

10499
class Trajectory(BaseModel):
105100
"""Represents a conversation trajectory with messages and optional scoring."""

flowllm/core/vector_store/chroma_vector_store.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def _build_chroma_filters(
8787
- Term filters: {"key": "value"} -> exact match
8888
- Range filters: {"key": {"gte": 1, "lte": 10}} -> range query
8989
- unique_id: Filters by document ID (stored separately from metadata)
90+
- Keys can use "metadata." prefix (will be stripped for ChromaDB)
9091
9192
Returns:
9293
Tuple of (where_clause, ids_filter):
@@ -108,6 +109,12 @@ def _build_chroma_filters(
108109
ids_filter = [filter_value]
109110
continue
110111

112+
# Strip "metadata." prefix if present (ChromaDB stores metadata fields directly)
113+
if key.startswith("metadata."):
114+
chroma_key = key[len("metadata.") :]
115+
else:
116+
chroma_key = key
117+
111118
if isinstance(filter_value, dict):
112119
# Range filter: {"gte": 1, "lte": 10}
113120
range_conditions = {}
@@ -120,10 +127,13 @@ def _build_chroma_filters(
120127
if "lt" in filter_value:
121128
range_conditions["$lt"] = filter_value["lt"]
122129
if range_conditions:
123-
where_conditions[key] = range_conditions
130+
where_conditions[chroma_key] = range_conditions
131+
elif isinstance(filter_value, list):
132+
# List filter: use $in operator for OR logic
133+
where_conditions[chroma_key] = {"$in": filter_value}
124134
else:
125135
# Term filter: direct value comparison
126-
where_conditions[key] = filter_value
136+
where_conditions[chroma_key] = filter_value
127137

128138
return (where_conditions if where_conditions else None, ids_filter)
129139

flowllm/core/vector_store/es_vector_store.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ def _build_es_filters(filter_dict: Optional[Dict[str, Any]] = None) -> List[Dict
158158
range_conditions["lt"] = filter_value["lt"]
159159
if range_conditions:
160160
filters.append({"range": {es_key: range_conditions}})
161+
elif isinstance(filter_value, list):
162+
# List filter: use terms query for OR logic
163+
filters.append({"terms": {es_key: filter_value}})
161164
else:
162165
# Term filter: direct value comparison
163166
filters.append({"term": {es_key: filter_value}})

flowllm/core/vector_store/memory_vector_store.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ def _matches_filters(node: VectorNode, filter_dict: dict = None) -> bool:
228228
range_match = False
229229
if not range_match:
230230
return False
231+
elif isinstance(filter_value, list):
232+
# List filter: value must match any item in the list (OR logic)
233+
if value not in filter_value:
234+
return False
231235
else:
232236
# Term filter: direct value comparison
233237
if value != filter_value:

flowllm/core/vector_store/pgvector_vector_store.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,16 @@ def _build_sql_filters(
218218
param_idx += 1
219219
if range_conditions:
220220
conditions.append(f"({' AND '.join(range_conditions)})")
221+
elif isinstance(filter_value, list):
222+
# List filter: use IN clause for OR logic
223+
if use_async:
224+
placeholders = ", ".join(f"${param_idx + i}" for i in range(len(filter_value)))
225+
conditions.append(f"{jsonb_path} IN ({placeholders})")
226+
else:
227+
placeholders = ", ".join(["%s"] * len(filter_value))
228+
conditions.append(f"{jsonb_path} IN ({placeholders})")
229+
params.extend([str(v) for v in filter_value])
230+
param_idx += len(filter_value)
221231
else:
222232
# Term filter: direct value comparison
223233
if use_async:

flowllm/core/vector_store/qdrant_vector_store.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def _build_qdrant_filters(filter_dict: Optional[Dict[str, Any]] = None):
181181
filter_dict = {"age": {"gte": 18, "lte": 65}}
182182
```
183183
"""
184-
from qdrant_client.http.models import FieldCondition, MatchValue, Range
184+
from qdrant_client.http.models import FieldCondition, MatchAny, MatchValue, Range
185185

186186
if not filter_dict:
187187
return None
@@ -215,6 +215,14 @@ def _build_qdrant_filters(filter_dict: Optional[Dict[str, Any]] = None):
215215
range=Range(**range_conditions),
216216
),
217217
)
218+
elif isinstance(filter_value, list):
219+
# List filter: use MatchAny for OR logic
220+
conditions.append(
221+
FieldCondition(
222+
key=qdrant_key,
223+
match=MatchAny(any=filter_value),
224+
),
225+
)
218226
else:
219227
# Term filter: direct value comparison
220228
conditions.append(

tests/test_memory_vector_store.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,23 @@ def create_sample_nodes(workspace_id: str, prefix: str = "") -> List[VectorNode]
167167
VectorNode(
168168
unique_id=f"{id_prefix}node3",
169169
workspace_id=workspace_id,
170+
content="Machine learning is a subset of artificial intelligence.",
171+
metadata={
172+
"node_type": "tech_new",
173+
"category": "ML",
174+
},
175+
),
176+
VectorNode(
177+
unique_id=f"{id_prefix}node4",
178+
workspace_id=workspace_id,
170179
content="I love eating delicious seafood, especially fresh fish.",
171180
metadata={
172181
"node_type": "food",
173182
"category": "preference",
174183
},
175184
),
176185
VectorNode(
177-
unique_id=f"{id_prefix}node4",
186+
unique_id=f"{id_prefix}node5",
178187
workspace_id=workspace_id,
179188
content="Deep learning uses neural networks with multiple layers.",
180189
metadata={
@@ -277,17 +286,20 @@ def test_search(self, workspace_id: str):
277286
def test_search_with_filter(self, workspace_id: str):
278287
"""Test vector search with filter."""
279288
logger.info("=" * 20 + " FILTER SEARCH TEST " + "=" * 20)
280-
filter_dict = {"metadata.node_type": "tech"}
289+
filter_dict = {"metadata.node_type": ["tech", "tech_new"]}
281290
results = self.client.search(
282291
"What is artificial intelligence?",
283292
workspace_id=workspace_id,
284293
top_k=5,
285294
filter_dict=filter_dict,
286295
)
287-
logger.info(f"Filtered search returned {len(results)} results (node_type=tech)")
296+
logger.info(f"Filtered search returned {len(results)} results (node_type in [tech, tech_new])")
288297
for i, r in enumerate(results, 1):
289298
logger.info(f"Filtered Result {i}: {r.model_dump(exclude={'vector'})}")
290-
assert r.metadata.get("node_type") == "tech", "All results should have node_type=tech"
299+
assert r.metadata.get("node_type") in [
300+
"tech",
301+
"tech_new",
302+
], "All results should have node_type in [tech, tech_new]"
291303

292304
def test_search_with_id(self, workspace_id: str):
293305
"""Test vector search by unique_id with empty query."""
@@ -503,17 +515,20 @@ async def test_search(self, workspace_id: str):
503515
async def test_search_with_filter(self, workspace_id: str):
504516
"""Test async vector search with filter."""
505517
logger.info("ASYNC - " + "=" * 20 + " FILTER SEARCH TEST " + "=" * 20)
506-
filter_dict = {"metadata.node_type": "tech"}
518+
filter_dict = {"metadata.node_type": ["tech", "tech_new"]}
507519
results = await self.client.async_search(
508520
"What is artificial intelligence?",
509521
workspace_id=workspace_id,
510522
top_k=5,
511523
filter_dict=filter_dict,
512524
)
513-
logger.info(f"Filtered search returned {len(results)} results (node_type=tech)")
525+
logger.info(f"Filtered search returned {len(results)} results (node_type in [tech, tech_new])")
514526
for i, r in enumerate(results, 1):
515527
logger.info(f"Filtered Result {i}: {r.model_dump(exclude={'vector'})}")
516-
assert r.metadata.get("node_type") == "tech", "All results should have node_type=tech"
528+
assert r.metadata.get("node_type") in [
529+
"tech",
530+
"tech_new",
531+
], "All results should have node_type in [tech, tech_new]"
517532

518533
async def test_search_with_id(self, workspace_id: str):
519534
"""Test async vector search by unique_id with empty query."""

0 commit comments

Comments
 (0)