Merge pull request #9 from ggozad/feat/document-meta-in-search

Include document uri and meta in search results
This commit is contained in:
Yiorgis Gozadinos 2025-06-28 09:41:37 +03:00 committed by GitHub
commit 7e540583ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 66 additions and 13 deletions

View file

@ -3,10 +3,12 @@ from pydantic import BaseModel
class Chunk(BaseModel): class Chunk(BaseModel):
""" """
Represents a document with an ID, content, and metadata. Represents a chunk with content, metadata, and optional document information.
""" """
id: int | None = None id: int | None = None
document_id: int document_id: int
content: str content: str
metadata: dict = {} metadata: dict = {}
document_uri: str | None = None
document_meta: dict = {}

View file

@ -240,9 +240,10 @@ class ChunkRepository(BaseRepository[Chunk]):
# Search for similar chunks using sqlite-vec # Search for similar chunks using sqlite-vec
cursor.execute( cursor.execute(
""" """
SELECT c.id, c.document_id, c.content, c.metadata, distance SELECT c.id, c.document_id, c.content, c.metadata, distance, d.uri, d.metadata as document_metadata
FROM chunk_embeddings FROM chunk_embeddings
JOIN chunks c ON c.id = chunk_embeddings.chunk_id JOIN chunks c ON c.id = chunk_embeddings.chunk_id
JOIN documents d ON c.document_id = d.id
WHERE embedding MATCH :embedding AND k = :k WHERE embedding MATCH :embedding AND k = :k
ORDER BY distance ORDER BY distance
""", """,
@ -257,10 +258,14 @@ class ChunkRepository(BaseRepository[Chunk]):
document_id=document_id, document_id=document_id,
content=content, content=content,
metadata=json.loads(metadata_json) if metadata_json else {}, metadata=json.loads(metadata_json) if metadata_json else {},
document_uri=document_uri,
document_meta=json.loads(document_metadata_json)
if document_metadata_json
else {},
), ),
1.0 / (1.0 + distance), 1.0 / (1.0 + distance),
) )
for chunk_id, document_id, content, metadata_json, distance in results for chunk_id, document_id, content, metadata_json, distance, document_uri, document_metadata_json in results
] ]
async def search_chunks_fts( async def search_chunks_fts(
@ -281,9 +286,10 @@ class ChunkRepository(BaseRepository[Chunk]):
# Search using FTS5 # Search using FTS5
cursor.execute( cursor.execute(
""" """
SELECT c.id, c.document_id, c.content, c.metadata, rank SELECT c.id, c.document_id, c.content, c.metadata, rank, d.uri, d.metadata as document_metadata
FROM chunks_fts FROM chunks_fts
JOIN chunks c ON c.id = chunks_fts.rowid JOIN chunks c ON c.id = chunks_fts.rowid
JOIN documents d ON c.document_id = d.id
WHERE chunks_fts MATCH :query WHERE chunks_fts MATCH :query
ORDER BY rank ORDER BY rank
LIMIT :limit LIMIT :limit
@ -300,10 +306,14 @@ class ChunkRepository(BaseRepository[Chunk]):
document_id=document_id, document_id=document_id,
content=content, content=content,
metadata=json.loads(metadata_json) if metadata_json else {}, metadata=json.loads(metadata_json) if metadata_json else {},
document_uri=document_uri,
document_meta=json.loads(document_metadata_json)
if document_metadata_json
else {},
), ),
-rank, -rank,
) )
for chunk_id, document_id, content, metadata_json, rank in results for chunk_id, document_id, content, metadata_json, rank, document_uri, document_metadata_json in results
# FTS5 rank is negative BM25 score # FTS5 rank is negative BM25 score
] ]
@ -368,9 +378,10 @@ class ChunkRepository(BaseRepository[Chunk]):
LEFT JOIN vector_search v ON a.id = v.id LEFT JOIN vector_search v ON a.id = v.id
LEFT JOIN fts_search f ON a.id = f.id LEFT JOIN fts_search f ON a.id = f.id
) )
SELECT id, document_id, content, metadata, rrf_score SELECT r.id, r.document_id, r.content, r.metadata, r.rrf_score, d.uri, d.metadata as document_metadata
FROM rrf_scores FROM rrf_scores r
ORDER BY rrf_score DESC JOIN documents d ON r.document_id = d.id
ORDER BY r.rrf_score DESC
LIMIT :limit LIMIT :limit
""", """,
{ {
@ -390,10 +401,14 @@ class ChunkRepository(BaseRepository[Chunk]):
document_id=document_id, document_id=document_id,
content=content, content=content,
metadata=json.loads(metadata_json) if metadata_json else {}, metadata=json.loads(metadata_json) if metadata_json else {},
document_uri=document_uri,
document_meta=json.loads(document_metadata_json)
if document_metadata_json
else {},
), ),
rrf_score, rrf_score,
) )
for chunk_id, document_id, content, metadata_json, rrf_score in results for chunk_id, document_id, content, metadata_json, rrf_score, document_uri, document_metadata_json in results
] ]
async def get_by_document_id(self, document_id: int) -> list[Chunk]: async def get_by_document_id(self, document_id: int) -> list[Chunk]:
@ -404,9 +419,11 @@ class ChunkRepository(BaseRepository[Chunk]):
cursor = self.store._connection.cursor() cursor = self.store._connection.cursor()
cursor.execute( cursor.execute(
""" """
SELECT id, document_id, content, metadata SELECT c.id, c.document_id, c.content, c.metadata, d.uri, d.metadata as document_metadata
FROM chunks WHERE document_id = :document_id FROM chunks c
ORDER BY JSON_EXTRACT(metadata, '$.order') JOIN documents d ON c.document_id = d.id
WHERE c.document_id = :document_id
ORDER BY JSON_EXTRACT(c.metadata, '$.order')
""", """,
{"document_id": document_id}, {"document_id": document_id},
) )
@ -418,6 +435,10 @@ class ChunkRepository(BaseRepository[Chunk]):
document_id=document_id, document_id=document_id,
content=content, content=content,
metadata=json.loads(metadata_json) if metadata_json else {}, metadata=json.loads(metadata_json) if metadata_json else {},
document_uri=document_uri,
document_meta=json.loads(document_metadata_json)
if document_metadata_json
else {},
) )
for chunk_id, document_id, content, metadata_json in rows for chunk_id, document_id, content, metadata_json, document_uri, document_metadata_json in rows
] ]

View file

@ -56,3 +56,33 @@ async def test_search_qa_corpus(qa_corpus: Dataset):
assert target_document.id in target_document_ids assert target_document.id in target_document_ids
store.close() store.close()
@pytest.mark.asyncio
async def test_chunks_include_document_info():
"""Test that search results include document URI and metadata."""
store = Store(":memory:")
doc_repo = DocumentRepository(store)
chunk_repo = ChunkRepository(store)
# Create a document with URI and metadata
document = Document(
content="This is a test document with some content for searching.",
uri="https://example.com/test.html",
metadata={"title": "Test Document", "author": "Test Author"},
)
created_document = await doc_repo.create(document)
# Search for chunks
results = await chunk_repo.search_chunks_hybrid("test document", limit=1)
assert len(results) > 0
chunk, score = results[0]
# Verify the chunk includes document information
assert chunk.document_uri == "https://example.com/test.html"
assert chunk.document_meta == {"title": "Test Document", "author": "Test Author"}
assert chunk.document_id == created_document.id
store.close()