sia-code 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sia_code-0.2.2 → sia_code-0.2.3}/PKG-INFO +1 -1
- {sia_code-0.2.2 → sia_code-0.2.3}/pyproject.toml +2 -2
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/__init__.py +1 -1
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/cli.py +12 -2
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/indexer/coordinator.py +1 -110
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/parser/concepts.py +151 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/parser/engine.py +10 -3
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/search/entity_extractor.py +7 -2
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/search/multi_hop.py +7 -1
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code.egg-info/PKG-INFO +1 -1
- {sia_code-0.2.2 → sia_code-0.2.3}/tests/test_basic.py +24 -3
- {sia_code-0.2.2 → sia_code-0.2.3}/README.md +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/setup.cfg +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/config.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/core/__init__.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/core/models.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/core/types.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/indexer/__init__.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/indexer/chunk_index.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/indexer/embedder.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/indexer/hash_cache.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/indexer/metrics.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/parser/__init__.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/parser/chunker.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/parser/languages/__init__.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/search/__init__.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/search/service.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/search/single_hop.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/storage/__init__.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/storage/backend.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code.egg-info/SOURCES.txt +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code.egg-info/dependency_links.txt +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code.egg-info/entry_points.txt +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code.egg-info/requires.txt +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/sia_code.egg-info/top_level.txt +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/tests/test_cli_integration.py +0 -0
- {sia_code-0.2.2 → sia_code-0.2.3}/tests/test_empty_code_fix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sia-code"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "Local-first codebase intelligence with semantic search, multi-hop research, and 12-language AST support"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -98,7 +98,7 @@ python_classes = "Test*"
|
|
|
98
98
|
python_functions = "test_*"
|
|
99
99
|
|
|
100
100
|
[tool.bumpversion]
|
|
101
|
-
current_version = "0.2.
|
|
101
|
+
current_version = "0.2.3"
|
|
102
102
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
103
103
|
serialize = ["{major}.{minor}.{patch}"]
|
|
104
104
|
commit = true
|
|
@@ -381,14 +381,24 @@ def index(
|
|
|
381
381
|
console.print("[dim]Re-indexing...[/dim]")
|
|
382
382
|
|
|
383
383
|
try:
|
|
384
|
-
# Perform incremental reindex
|
|
384
|
+
# Perform incremental reindex using v2
|
|
385
385
|
from .indexer.hash_cache import HashCache
|
|
386
|
+
from .indexer.chunk_index import ChunkIndex
|
|
386
387
|
|
|
387
388
|
cache_path = sia_dir / "cache" / "file_hashes.json"
|
|
388
389
|
cache = HashCache(cache_path)
|
|
389
390
|
|
|
391
|
+
chunk_index_path = sia_dir / "chunk_index.json"
|
|
392
|
+
chunk_index = ChunkIndex(chunk_index_path)
|
|
393
|
+
chunk_index.load()
|
|
394
|
+
|
|
390
395
|
coordinator = IndexingCoordinator(backend=backend, config=config)
|
|
391
|
-
stats = coordinator.
|
|
396
|
+
stats = coordinator.index_directory_incremental_v2(
|
|
397
|
+
Path(path), cache, chunk_index, progress_callback=None
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Save updated chunk index
|
|
401
|
+
chunk_index.save()
|
|
392
402
|
|
|
393
403
|
console.print(
|
|
394
404
|
f"[green]✓[/green] Re-indexed {stats['files_indexed']} files, {stats['chunks_indexed']} chunks"
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"""Indexing coordinator - orchestrates parse → chunk → store."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
import hashlib
|
|
5
4
|
import logging
|
|
6
5
|
import time
|
|
7
6
|
import os
|
|
8
7
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
9
|
-
from typing import
|
|
8
|
+
from typing import Callable
|
|
10
9
|
|
|
11
10
|
import pathspec
|
|
12
11
|
|
|
@@ -485,99 +484,6 @@ class IndexingCoordinator:
|
|
|
485
484
|
|
|
486
485
|
return stats
|
|
487
486
|
|
|
488
|
-
def index_directory_incremental(self, directory: Path, cache: HashCache) -> dict:
|
|
489
|
-
"""Index only changed files using hash cache.
|
|
490
|
-
|
|
491
|
-
Args:
|
|
492
|
-
directory: Root directory to index
|
|
493
|
-
cache: Hash cache for change detection
|
|
494
|
-
|
|
495
|
-
Returns:
|
|
496
|
-
Statistics dictionary
|
|
497
|
-
"""
|
|
498
|
-
# Start performance tracking
|
|
499
|
-
metrics = PerformanceMetrics()
|
|
500
|
-
|
|
501
|
-
files = self._discover_files(directory)
|
|
502
|
-
|
|
503
|
-
stats = {
|
|
504
|
-
"total_files": len(files),
|
|
505
|
-
"changed_files": 0,
|
|
506
|
-
"skipped_files": 0,
|
|
507
|
-
"indexed_files": 0,
|
|
508
|
-
"total_chunks": 0,
|
|
509
|
-
"errors": [],
|
|
510
|
-
"metrics": None, # Will be filled at end
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
for file_path in files:
|
|
514
|
-
# Check if file changed
|
|
515
|
-
if not cache.has_changed(file_path):
|
|
516
|
-
stats["skipped_files"] += 1
|
|
517
|
-
continue
|
|
518
|
-
|
|
519
|
-
stats["changed_files"] += 1
|
|
520
|
-
|
|
521
|
-
try:
|
|
522
|
-
language = Language.from_extension(file_path.suffix)
|
|
523
|
-
|
|
524
|
-
if not self.chunker.engine.is_supported(language):
|
|
525
|
-
logger.debug(f"Skipping unsupported language: {file_path}")
|
|
526
|
-
continue
|
|
527
|
-
|
|
528
|
-
# Get old chunk IDs for this file
|
|
529
|
-
old_chunk_ids = cache.get_chunks(file_path)
|
|
530
|
-
|
|
531
|
-
# Delete old chunks (placeholder - Memvid doesn't support direct delete)
|
|
532
|
-
if old_chunk_ids:
|
|
533
|
-
logger.debug(
|
|
534
|
-
f"Old chunks exist for {file_path} ({len(old_chunk_ids)}), "
|
|
535
|
-
"but cannot delete (Memvid limitation)"
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
# Chunk the file with retry
|
|
539
|
-
chunks, error = self._index_file_with_retry(file_path, language)
|
|
540
|
-
|
|
541
|
-
if error:
|
|
542
|
-
stats["errors"].append(f"{file_path}: {error}")
|
|
543
|
-
metrics.errors_count += 1
|
|
544
|
-
continue
|
|
545
|
-
|
|
546
|
-
if chunks:
|
|
547
|
-
# Track file size
|
|
548
|
-
try:
|
|
549
|
-
metrics.bytes_processed += file_path.stat().st_size
|
|
550
|
-
except OSError:
|
|
551
|
-
pass
|
|
552
|
-
|
|
553
|
-
# Store new chunks
|
|
554
|
-
chunk_ids = self.backend.store_chunks_batch(chunks)
|
|
555
|
-
|
|
556
|
-
# Update cache with new chunk IDs
|
|
557
|
-
cache.update(file_path, cast(list[str], chunk_ids))
|
|
558
|
-
|
|
559
|
-
stats["indexed_files"] += 1
|
|
560
|
-
stats["total_chunks"] += len(chunks)
|
|
561
|
-
metrics.files_processed += 1
|
|
562
|
-
metrics.chunks_created += len(chunks)
|
|
563
|
-
logger.info(f"Re-indexed {file_path}: {len(chunks)} chunks")
|
|
564
|
-
|
|
565
|
-
except Exception as e:
|
|
566
|
-
error_msg = f"Unexpected error: {str(e)}"
|
|
567
|
-
stats["errors"].append(f"{file_path}: {error_msg}")
|
|
568
|
-
metrics.errors_count += 1
|
|
569
|
-
logger.exception(f"Unexpected error indexing {file_path}")
|
|
570
|
-
|
|
571
|
-
# Save updated cache
|
|
572
|
-
cache.save()
|
|
573
|
-
|
|
574
|
-
# Finalize metrics
|
|
575
|
-
metrics.finish()
|
|
576
|
-
stats["metrics"] = metrics.to_dict()
|
|
577
|
-
logger.info(f"Incremental indexing complete: {metrics}")
|
|
578
|
-
|
|
579
|
-
return stats
|
|
580
|
-
|
|
581
487
|
def compact_index(
|
|
582
488
|
self, directory: Path, chunk_index: ChunkIndex, threshold: float = 0.2
|
|
583
489
|
) -> dict:
|
|
@@ -722,18 +628,3 @@ class IndexingCoordinator:
|
|
|
722
628
|
logger.info(f"Compaction complete: {metrics}")
|
|
723
629
|
|
|
724
630
|
return stats
|
|
725
|
-
|
|
726
|
-
def get_file_hash(self, file_path: Path) -> str:
|
|
727
|
-
"""Calculate file hash for change detection.
|
|
728
|
-
|
|
729
|
-
Args:
|
|
730
|
-
file_path: Path to file
|
|
731
|
-
|
|
732
|
-
Returns:
|
|
733
|
-
MD5 hash of file
|
|
734
|
-
"""
|
|
735
|
-
hasher = hashlib.md5()
|
|
736
|
-
with open(file_path, "rb") as f:
|
|
737
|
-
for chunk in iter(lambda: f.read(4096), b""):
|
|
738
|
-
hasher.update(chunk)
|
|
739
|
-
return hasher.hexdigest()
|
|
@@ -134,6 +134,157 @@ class ConceptExtractor:
|
|
|
134
134
|
traverse(root)
|
|
135
135
|
return concepts
|
|
136
136
|
|
|
137
|
+
def _extract_javascript_concepts(
|
|
138
|
+
self, root: Node, source_code: bytes
|
|
139
|
+
) -> list[UniversalConcept]:
|
|
140
|
+
"""Extract JavaScript/TypeScript-specific concepts."""
|
|
141
|
+
concepts = []
|
|
142
|
+
|
|
143
|
+
def traverse(node: Node, parent_class: str | None = None):
|
|
144
|
+
# Function declarations: function foo() {}
|
|
145
|
+
if node.type == "function_declaration":
|
|
146
|
+
name_node = node.child_by_field_name("name")
|
|
147
|
+
if name_node:
|
|
148
|
+
symbol = source_code[name_node.start_byte : name_node.end_byte].decode("utf-8")
|
|
149
|
+
concepts.append(
|
|
150
|
+
UniversalConcept(
|
|
151
|
+
concept_type=ConceptType.DEFINITION,
|
|
152
|
+
chunk_type=ChunkType.FUNCTION,
|
|
153
|
+
symbol=symbol,
|
|
154
|
+
start_line=LineNumber(node.start_point[0] + 1),
|
|
155
|
+
end_line=LineNumber(node.end_point[0] + 1),
|
|
156
|
+
start_byte=ByteOffset(node.start_byte),
|
|
157
|
+
end_byte=ByteOffset(node.end_byte),
|
|
158
|
+
code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
|
|
159
|
+
parent_header=parent_class,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Arrow functions and function expressions: const foo = () => {}
|
|
164
|
+
elif node.type in ("arrow_function", "function_expression", "function"):
|
|
165
|
+
# Try to find the parent variable declarator to get the name
|
|
166
|
+
parent = node.parent
|
|
167
|
+
symbol = "anonymous"
|
|
168
|
+
if parent and parent.type == "variable_declarator":
|
|
169
|
+
name_node = parent.child_by_field_name("name")
|
|
170
|
+
if name_node:
|
|
171
|
+
symbol = source_code[name_node.start_byte : name_node.end_byte].decode(
|
|
172
|
+
"utf-8"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
concepts.append(
|
|
176
|
+
UniversalConcept(
|
|
177
|
+
concept_type=ConceptType.DEFINITION,
|
|
178
|
+
chunk_type=ChunkType.FUNCTION,
|
|
179
|
+
symbol=symbol,
|
|
180
|
+
start_line=LineNumber(node.start_point[0] + 1),
|
|
181
|
+
end_line=LineNumber(node.end_point[0] + 1),
|
|
182
|
+
start_byte=ByteOffset(node.start_byte),
|
|
183
|
+
end_byte=ByteOffset(node.end_byte),
|
|
184
|
+
code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
|
|
185
|
+
parent_header=parent_class,
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Class declarations
|
|
190
|
+
elif node.type == "class_declaration":
|
|
191
|
+
name_node = node.child_by_field_name("name")
|
|
192
|
+
if name_node:
|
|
193
|
+
symbol = source_code[name_node.start_byte : name_node.end_byte].decode("utf-8")
|
|
194
|
+
concepts.append(
|
|
195
|
+
UniversalConcept(
|
|
196
|
+
concept_type=ConceptType.DEFINITION,
|
|
197
|
+
chunk_type=ChunkType.CLASS,
|
|
198
|
+
symbol=symbol,
|
|
199
|
+
start_line=LineNumber(node.start_point[0] + 1),
|
|
200
|
+
end_line=LineNumber(node.end_point[0] + 1),
|
|
201
|
+
start_byte=ByteOffset(node.start_byte),
|
|
202
|
+
end_byte=ByteOffset(node.end_byte),
|
|
203
|
+
code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
# Traverse children with class context
|
|
207
|
+
for child in node.children:
|
|
208
|
+
traverse(child, parent_class=symbol)
|
|
209
|
+
return # Don't traverse again
|
|
210
|
+
|
|
211
|
+
# Method definitions (inside classes)
|
|
212
|
+
elif node.type in ("method_definition", "public_field_definition"):
|
|
213
|
+
name_node = node.child_by_field_name("name")
|
|
214
|
+
if name_node:
|
|
215
|
+
symbol = source_code[name_node.start_byte : name_node.end_byte].decode("utf-8")
|
|
216
|
+
concepts.append(
|
|
217
|
+
UniversalConcept(
|
|
218
|
+
concept_type=ConceptType.DEFINITION,
|
|
219
|
+
chunk_type=ChunkType.METHOD if parent_class else ChunkType.FUNCTION,
|
|
220
|
+
symbol=symbol,
|
|
221
|
+
start_line=LineNumber(node.start_point[0] + 1),
|
|
222
|
+
end_line=LineNumber(node.end_point[0] + 1),
|
|
223
|
+
start_byte=ByteOffset(node.start_byte),
|
|
224
|
+
end_byte=ByteOffset(node.end_byte),
|
|
225
|
+
code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
|
|
226
|
+
parent_header=parent_class,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# TypeScript-specific: interface declarations
|
|
231
|
+
elif node.type == "interface_declaration":
|
|
232
|
+
name_node = node.child_by_field_name("name")
|
|
233
|
+
if name_node:
|
|
234
|
+
symbol = source_code[name_node.start_byte : name_node.end_byte].decode("utf-8")
|
|
235
|
+
concepts.append(
|
|
236
|
+
UniversalConcept(
|
|
237
|
+
concept_type=ConceptType.DEFINITION,
|
|
238
|
+
chunk_type=ChunkType.CLASS, # Treat interfaces like classes
|
|
239
|
+
symbol=symbol,
|
|
240
|
+
start_line=LineNumber(node.start_point[0] + 1),
|
|
241
|
+
end_line=LineNumber(node.end_point[0] + 1),
|
|
242
|
+
start_byte=ByteOffset(node.start_byte),
|
|
243
|
+
end_byte=ByteOffset(node.end_byte),
|
|
244
|
+
code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# TypeScript-specific: type alias declarations
|
|
249
|
+
elif node.type == "type_alias_declaration":
|
|
250
|
+
name_node = node.child_by_field_name("name")
|
|
251
|
+
if name_node:
|
|
252
|
+
symbol = source_code[name_node.start_byte : name_node.end_byte].decode("utf-8")
|
|
253
|
+
concepts.append(
|
|
254
|
+
UniversalConcept(
|
|
255
|
+
concept_type=ConceptType.DEFINITION,
|
|
256
|
+
chunk_type=ChunkType.CLASS, # Treat type aliases like classes
|
|
257
|
+
symbol=symbol,
|
|
258
|
+
start_line=LineNumber(node.start_point[0] + 1),
|
|
259
|
+
end_line=LineNumber(node.end_point[0] + 1),
|
|
260
|
+
start_byte=ByteOffset(node.start_byte),
|
|
261
|
+
end_byte=ByteOffset(node.end_byte),
|
|
262
|
+
code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Comments
|
|
267
|
+
elif node.type == "comment":
|
|
268
|
+
concepts.append(
|
|
269
|
+
UniversalConcept(
|
|
270
|
+
concept_type=ConceptType.COMMENT,
|
|
271
|
+
chunk_type=ChunkType.COMMENT,
|
|
272
|
+
symbol="comment",
|
|
273
|
+
start_line=LineNumber(node.start_point[0] + 1),
|
|
274
|
+
end_line=LineNumber(node.end_point[0] + 1),
|
|
275
|
+
start_byte=ByteOffset(node.start_byte),
|
|
276
|
+
end_byte=ByteOffset(node.end_byte),
|
|
277
|
+
code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Traverse children
|
|
282
|
+
for child in node.children:
|
|
283
|
+
traverse(child, parent_class)
|
|
284
|
+
|
|
285
|
+
traverse(root)
|
|
286
|
+
return concepts
|
|
287
|
+
|
|
137
288
|
def _extract_generic_concepts(self, root: Node, source_code: bytes) -> list[UniversalConcept]:
|
|
138
289
|
"""Extract concepts from C-like languages using common node types."""
|
|
139
290
|
concepts = []
|
|
@@ -100,8 +100,11 @@ class TreeSitterEngine:
|
|
|
100
100
|
with open(file_path, "rb") as f:
|
|
101
101
|
source_code = f.read()
|
|
102
102
|
return self.parse_code(source_code, language)
|
|
103
|
-
except Exception:
|
|
104
|
-
#
|
|
103
|
+
except Exception as e:
|
|
104
|
+
# Log parse failures for debugging
|
|
105
|
+
import logging
|
|
106
|
+
|
|
107
|
+
logging.getLogger(__name__).debug(f"Parse failed for {file_path}: {e}")
|
|
105
108
|
return None
|
|
106
109
|
|
|
107
110
|
def parse_code(self, source_code: bytes | str, language: PciLanguage):
|
|
@@ -116,7 +119,11 @@ class TreeSitterEngine:
|
|
|
116
119
|
parser = self._parsers[language]
|
|
117
120
|
tree = parser.parse(source_code)
|
|
118
121
|
return tree.root_node
|
|
119
|
-
except Exception:
|
|
122
|
+
except Exception as e:
|
|
123
|
+
# Log parse failures for debugging
|
|
124
|
+
import logging
|
|
125
|
+
|
|
126
|
+
logging.getLogger(__name__).debug(f"Parse code failed for {language}: {e}")
|
|
120
127
|
return None
|
|
121
128
|
|
|
122
129
|
def is_supported(self, language: PciLanguage) -> bool:
|
|
@@ -52,8 +52,13 @@ class EntityExtractor:
|
|
|
52
52
|
elif chunk.language in [Language.JAVASCRIPT, Language.TYPESCRIPT, Language.TSX]:
|
|
53
53
|
entities.extend(self._extract_js_entities(root, chunk))
|
|
54
54
|
|
|
55
|
-
except Exception:
|
|
56
|
-
#
|
|
55
|
+
except Exception as e:
|
|
56
|
+
# Log extraction failures for debugging
|
|
57
|
+
import logging
|
|
58
|
+
|
|
59
|
+
logging.getLogger(__name__).debug(
|
|
60
|
+
f"Entity extraction failed for chunk {chunk.symbol}: {e}"
|
|
61
|
+
)
|
|
57
62
|
pass
|
|
58
63
|
|
|
59
64
|
return entities
|
|
@@ -107,7 +107,13 @@ class MultiHopSearchStrategy:
|
|
|
107
107
|
# Search for this entity
|
|
108
108
|
try:
|
|
109
109
|
entity_results = self.backend.search_lexical(entity.name, k=3)
|
|
110
|
-
except Exception:
|
|
110
|
+
except Exception as e:
|
|
111
|
+
# Log search failures for debugging
|
|
112
|
+
import logging
|
|
113
|
+
|
|
114
|
+
logging.getLogger(__name__).debug(
|
|
115
|
+
f"Entity search failed for {entity.name}: {e}"
|
|
116
|
+
)
|
|
111
117
|
continue
|
|
112
118
|
|
|
113
119
|
# Process results
|
|
@@ -98,7 +98,9 @@ class TestMemvidBackend:
|
|
|
98
98
|
backend.store_chunks_batch(sample_chunks)
|
|
99
99
|
|
|
100
100
|
results = backend.search_lexical("search", k=5)
|
|
101
|
-
|
|
101
|
+
|
|
102
|
+
# Respect the k parameter
|
|
103
|
+
assert 1 <= len(results) <= 5
|
|
102
104
|
|
|
103
105
|
# Check result structure
|
|
104
106
|
for result in results:
|
|
@@ -106,23 +108,42 @@ class TestMemvidBackend:
|
|
|
106
108
|
assert result.chunk.symbol is not None
|
|
107
109
|
assert result.score > 0 # BM25 scores are positive
|
|
108
110
|
|
|
111
|
+
# Verify results are sorted by score (descending)
|
|
112
|
+
scores = [r.score for r in results]
|
|
113
|
+
assert scores == sorted(scores, reverse=True), (
|
|
114
|
+
"Results should be sorted by score (descending)"
|
|
115
|
+
)
|
|
116
|
+
|
|
109
117
|
def test_lexical_search_multiple_terms(self, backend, sample_chunks):
|
|
110
118
|
"""Test lexical search with multiple terms."""
|
|
111
119
|
backend.store_chunks_batch(sample_chunks)
|
|
112
120
|
|
|
113
121
|
results = backend.search_lexical("semantic code chunk", k=5)
|
|
114
|
-
|
|
122
|
+
|
|
123
|
+
# Respect the k parameter
|
|
124
|
+
assert 1 <= len(results) <= 5
|
|
125
|
+
|
|
126
|
+
# Verify results are sorted by relevance
|
|
127
|
+
scores = [r.score for r in results]
|
|
128
|
+
assert scores == sorted(scores, reverse=True)
|
|
115
129
|
|
|
116
130
|
def test_lexical_search_language(self, backend, sample_chunks):
|
|
117
131
|
"""Test lexical search finds language-related content."""
|
|
118
132
|
backend.store_chunks_batch(sample_chunks)
|
|
119
133
|
|
|
120
134
|
results = backend.search_lexical("languages", k=5)
|
|
121
|
-
|
|
135
|
+
|
|
136
|
+
# Respect the k parameter
|
|
137
|
+
assert 1 <= len(results) <= 5
|
|
138
|
+
|
|
122
139
|
# Should find the Language class
|
|
123
140
|
symbols = [r.chunk.symbol for r in results]
|
|
124
141
|
assert "Language" in symbols
|
|
125
142
|
|
|
143
|
+
# Verify scores are descending
|
|
144
|
+
scores = [r.score for r in results]
|
|
145
|
+
assert scores == sorted(scores, reverse=True)
|
|
146
|
+
|
|
126
147
|
|
|
127
148
|
class TestChunkModel:
|
|
128
149
|
"""Test Chunk model validation."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|