vexor 0.19.0a1__py3-none-any.whl → 0.21.0__py3-none-any.whl

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.
@@ -7,12 +7,15 @@ from functools import lru_cache
7
7
  from pathlib import Path
8
8
  import json
9
9
  import re
10
+ import numpy as np
10
11
  from typing import Sequence, TYPE_CHECKING
11
12
  from urllib import error as urlerror
12
13
  from urllib import request as urlrequest
13
14
 
14
15
  from ..config import (
15
16
  DEFAULT_EMBED_CONCURRENCY,
17
+ DEFAULT_EXTRACT_BACKEND,
18
+ DEFAULT_EXTRACT_CONCURRENCY,
16
19
  DEFAULT_FLASHRANK_MAX_LENGTH,
17
20
  DEFAULT_FLASHRANK_MODEL,
18
21
  DEFAULT_RERANK,
@@ -45,7 +48,11 @@ class SearchRequest:
45
48
  exclude_patterns: tuple[str, ...]
46
49
  extensions: tuple[str, ...]
47
50
  auto_index: bool = True
51
+ temporary_index: bool = False
52
+ no_cache: bool = False
48
53
  embed_concurrency: int = DEFAULT_EMBED_CONCURRENCY
54
+ extract_concurrency: int = DEFAULT_EXTRACT_CONCURRENCY
55
+ extract_backend: str = DEFAULT_EXTRACT_BACKEND
49
56
  rerank: str = DEFAULT_RERANK
50
57
  flashrank_model: str | None = None
51
58
  remote_rerank: RemoteRerankConfig | None = None
@@ -105,6 +112,20 @@ def _normalize_by_max(scores: Sequence[float]) -> list[float]:
105
112
  return [score / max_score for score in scores]
106
113
 
107
114
 
115
+ def _resolve_rerank_candidates(top_k: int) -> int:
116
+ candidate = int(top_k * 2)
117
+ return max(20, min(candidate, 150))
118
+
119
+
120
+ def _top_indices(scores: np.ndarray, limit: int) -> list[int]:
121
+ if limit <= 0:
122
+ return []
123
+ if limit >= scores.size:
124
+ return sorted(range(scores.size), key=lambda idx: (-scores[idx], idx))
125
+ indices = np.argpartition(-scores, limit - 1)[:limit]
126
+ return sorted(indices.tolist(), key=lambda idx: (-scores[idx], idx))
127
+
128
+
108
129
  def _bm25_scores(
109
130
  query_tokens: Sequence[str],
110
131
  documents: Sequence[Sequence[str]],
@@ -336,9 +357,13 @@ def _apply_remote_rerank(
336
357
  def perform_search(request: SearchRequest) -> SearchResponse:
337
358
  """Execute the semantic search flow and return ranked results."""
338
359
 
360
+ if request.temporary_index or request.no_cache:
361
+ return _perform_search_with_temporary_index(request)
362
+
339
363
  from ..cache import ( # local import
340
364
  embedding_cache_key,
341
365
  list_cache_entries,
366
+ load_chunk_metadata,
342
367
  load_embedding_cache,
343
368
  load_index_vectors,
344
369
  load_query_vector,
@@ -375,12 +400,15 @@ def perform_search(request: SearchRequest) -> SearchResponse:
375
400
  model_name=request.model_name,
376
401
  batch_size=request.batch_size,
377
402
  embed_concurrency=request.embed_concurrency,
403
+ extract_concurrency=request.extract_concurrency,
404
+ extract_backend=request.extract_backend,
378
405
  provider=request.provider,
379
406
  base_url=request.base_url,
380
407
  api_key=request.api_key,
381
408
  local_cuda=request.local_cuda,
382
409
  exclude_patterns=request.exclude_patterns,
383
410
  extensions=request.extensions,
411
+ no_cache=request.no_cache,
384
412
  )
385
413
  if result.status == IndexStatus.EMPTY:
386
414
  return SearchResponse(
@@ -435,6 +463,7 @@ def perform_search(request: SearchRequest) -> SearchResponse:
435
463
 
436
464
  file_snapshot = metadata.get("files", [])
437
465
  chunk_entries = metadata.get("chunks", [])
466
+ chunk_ids = metadata.get("chunk_ids", [])
438
467
  stale = bool(file_snapshot) and not is_cache_current(
439
468
  request.directory,
440
469
  request.include_hidden,
@@ -455,12 +484,15 @@ def perform_search(request: SearchRequest) -> SearchResponse:
455
484
  model_name=request.model_name,
456
485
  batch_size=request.batch_size,
457
486
  embed_concurrency=request.embed_concurrency,
487
+ extract_concurrency=request.extract_concurrency,
488
+ extract_backend=request.extract_backend,
458
489
  provider=request.provider,
459
490
  base_url=request.base_url,
460
491
  api_key=request.api_key,
461
492
  local_cuda=request.local_cuda,
462
493
  exclude_patterns=index_excludes,
463
494
  extensions=index_extensions,
495
+ no_cache=request.no_cache,
464
496
  )
465
497
  if result.status == IndexStatus.EMPTY:
466
498
  return SearchResponse(
@@ -529,7 +561,6 @@ def perform_search(request: SearchRequest) -> SearchResponse:
529
561
  index_empty=True,
530
562
  )
531
563
 
532
- from sklearn.metrics.pairwise import cosine_similarity # local import
533
564
  from ..search import SearchResult, VexorSearcher # local import
534
565
  searcher = VexorSearcher(
535
566
  model_name=request.model_name,
@@ -542,9 +573,9 @@ def perform_search(request: SearchRequest) -> SearchResponse:
542
573
  )
543
574
  query_vector = None
544
575
  query_hash = None
545
- query_text_hash = embedding_cache_key(request.query)
576
+ query_text_hash = None
546
577
  index_id = metadata.get("index_id")
547
- if index_id is not None:
578
+ if index_id is not None and not request.no_cache:
548
579
  query_hash = query_cache_key(request.query, request.model_name)
549
580
  try:
550
581
  query_vector = load_query_vector(int(index_id), query_hash)
@@ -554,7 +585,8 @@ def perform_search(request: SearchRequest) -> SearchResponse:
554
585
  if query_vector is not None and query_vector.size != file_vectors.shape[1]:
555
586
  query_vector = None
556
587
 
557
- if query_vector is None:
588
+ if query_vector is None and not request.no_cache:
589
+ query_text_hash = embedding_cache_key(request.query)
558
590
  cached = load_embedding_cache(request.model_name, [query_text_hash])
559
591
  query_vector = cached.get(query_text_hash)
560
592
  if query_vector is not None and query_vector.size != file_vectors.shape[1]:
@@ -562,25 +594,57 @@ def perform_search(request: SearchRequest) -> SearchResponse:
562
594
 
563
595
  if query_vector is None:
564
596
  query_vector = searcher.embed_texts([request.query])[0]
565
- try:
566
- store_embedding_cache(
567
- model=request.model_name,
568
- embeddings={query_text_hash: query_vector},
569
- )
570
- except Exception: # pragma: no cover - best-effort cache storage
571
- pass
572
- if query_vector is not None and index_id is not None and query_hash is not None:
597
+ if not request.no_cache:
598
+ if query_text_hash is None:
599
+ query_text_hash = embedding_cache_key(request.query)
600
+ try:
601
+ store_embedding_cache(
602
+ model=request.model_name,
603
+ embeddings={query_text_hash: query_vector},
604
+ )
605
+ except Exception: # pragma: no cover - best-effort cache storage
606
+ pass
607
+ if (
608
+ not request.no_cache
609
+ and query_vector is not None
610
+ and index_id is not None
611
+ and query_hash is not None
612
+ ):
573
613
  try:
574
614
  store_query_vector(int(index_id), query_hash, request.query, query_vector)
575
615
  except Exception: # pragma: no cover - best-effort cache storage
576
616
  pass
577
- similarities = cosine_similarity(
578
- query_vector.reshape(1, -1),
579
- file_vectors,
580
- )[0]
581
- scored = []
582
- for idx, (path, score) in enumerate(zip(paths, similarities)):
583
- chunk_meta = chunk_entries[idx] if idx < len(chunk_entries) else {}
617
+ reranker = None
618
+ rerank = (request.rerank or DEFAULT_RERANK).strip().lower()
619
+ use_rerank = rerank in {"bm25", "flashrank", "remote"}
620
+ if use_rerank:
621
+ candidate_limit = _resolve_rerank_candidates(request.top_k)
622
+ else:
623
+ candidate_limit = request.top_k
624
+ candidate_count = min(len(paths), candidate_limit)
625
+
626
+ query_vector = np.asarray(query_vector, dtype=np.float32).ravel()
627
+ similarities = np.asarray(file_vectors @ query_vector, dtype=np.float32)
628
+ top_indices = _top_indices(similarities, candidate_count)
629
+ chunk_meta_by_id: dict[int, dict] = {}
630
+ if chunk_ids:
631
+ candidate_ids = [
632
+ chunk_ids[idx] for idx in top_indices if idx < len(chunk_ids)
633
+ ]
634
+ if candidate_ids:
635
+ try:
636
+ chunk_meta_by_id = load_chunk_metadata(candidate_ids)
637
+ except Exception: # pragma: no cover - best-effort metadata lookup
638
+ chunk_meta_by_id = {}
639
+ scored: list[SearchResult] = []
640
+ for idx in top_indices:
641
+ path = paths[idx]
642
+ score = similarities[idx]
643
+ chunk_meta = {}
644
+ if chunk_ids and idx < len(chunk_ids):
645
+ chunk_meta = chunk_meta_by_id.get(chunk_ids[idx], {})
646
+ elif idx < len(chunk_entries):
647
+ chunk_meta = chunk_entries[idx]
584
648
  start_line = chunk_meta.get("start_line")
585
649
  end_line = chunk_meta.get("end_line")
586
650
  scored.append(
@@ -593,12 +657,138 @@ def perform_search(request: SearchRequest) -> SearchResponse:
593
657
  end_line=int(end_line) if end_line is not None else None,
594
658
  )
595
659
  )
596
- scored.sort(key=lambda item: item.score, reverse=True)
660
+ if use_rerank:
661
+ candidates = scored
662
+ if rerank == "bm25":
663
+ candidates = _apply_bm25_rerank(request.query, candidates)
664
+ reranker = "bm25"
665
+ elif rerank == "flashrank":
666
+ candidates = _apply_flashrank_rerank(
667
+ request.query,
668
+ candidates,
669
+ request.flashrank_model,
670
+ )
671
+ reranker = "flashrank"
672
+ else:
673
+ candidates = _apply_remote_rerank(
674
+ request.query,
675
+ candidates,
676
+ request.remote_rerank,
677
+ )
678
+ reranker = "remote"
679
+ results = candidates[: request.top_k]
680
+ else:
681
+ results = scored[: request.top_k]
682
+ return SearchResponse(
683
+ base_path=request.directory,
684
+ backend=searcher.device,
685
+ results=results,
686
+ is_stale=stale,
687
+ index_empty=False,
688
+ reranker=reranker,
689
+ )
690
+
691
+
692
+ def _perform_search_with_temporary_index(request: SearchRequest) -> SearchResponse:
693
+ from .index_service import build_index_in_memory # local import
694
+
695
+ paths, file_vectors, metadata = build_index_in_memory(
696
+ request.directory,
697
+ include_hidden=request.include_hidden,
698
+ respect_gitignore=request.respect_gitignore,
699
+ mode=request.mode,
700
+ recursive=request.recursive,
701
+ model_name=request.model_name,
702
+ batch_size=request.batch_size,
703
+ embed_concurrency=request.embed_concurrency,
704
+ extract_concurrency=request.extract_concurrency,
705
+ extract_backend=request.extract_backend,
706
+ provider=request.provider,
707
+ base_url=request.base_url,
708
+ api_key=request.api_key,
709
+ local_cuda=request.local_cuda,
710
+ exclude_patterns=request.exclude_patterns,
711
+ extensions=request.extensions,
712
+ no_cache=request.no_cache,
713
+ )
714
+
715
+ if not len(paths):
716
+ return SearchResponse(
717
+ base_path=request.directory,
718
+ backend=None,
719
+ results=[],
720
+ is_stale=False,
721
+ index_empty=True,
722
+ )
723
+
724
+ from ..search import SearchResult, VexorSearcher # local import
725
+
726
+ searcher = VexorSearcher(
727
+ model_name=request.model_name,
728
+ batch_size=request.batch_size,
729
+ embed_concurrency=request.embed_concurrency,
730
+ provider=request.provider,
731
+ base_url=request.base_url,
732
+ api_key=request.api_key,
733
+ local_cuda=request.local_cuda,
734
+ )
735
+ query_vector = None
736
+ query_text_hash = None
737
+ if not request.no_cache:
738
+ from ..cache import embedding_cache_key, load_embedding_cache, store_embedding_cache
739
+
740
+ query_text_hash = embedding_cache_key(request.query)
741
+ cached = load_embedding_cache(request.model_name, [query_text_hash])
742
+ query_vector = cached.get(query_text_hash)
743
+ if query_vector is not None and query_vector.size != file_vectors.shape[1]:
744
+ query_vector = None
745
+
746
+ if query_vector is None:
747
+ query_vector = searcher.embed_texts([request.query])[0]
748
+ if not request.no_cache:
749
+ if query_text_hash is None:
750
+ from ..cache import embedding_cache_key, store_embedding_cache
751
+
752
+ query_text_hash = embedding_cache_key(request.query)
753
+ try:
754
+ store_embedding_cache(
755
+ model=request.model_name,
756
+ embeddings={query_text_hash: query_vector},
757
+ )
758
+ except Exception: # pragma: no cover - best-effort cache storage
759
+ pass
597
760
  reranker = None
598
761
  rerank = (request.rerank or DEFAULT_RERANK).strip().lower()
599
- if rerank in {"bm25", "flashrank", "remote"}:
600
- candidate_count = min(len(scored), request.top_k * 2)
601
- candidates = scored[:candidate_count]
762
+ use_rerank = rerank in {"bm25", "flashrank", "remote"}
763
+ if use_rerank:
764
+ candidate_limit = _resolve_rerank_candidates(request.top_k)
765
+ else:
766
+ candidate_limit = request.top_k
767
+ candidate_count = min(len(paths), candidate_limit)
768
+
769
+ query_vector = np.asarray(query_vector, dtype=np.float32).ravel()
770
+ similarities = np.asarray(file_vectors @ query_vector, dtype=np.float32)
771
+ top_indices = _top_indices(similarities, candidate_count)
772
+ chunk_entries = metadata.get("chunks", [])
773
+ scored: list[SearchResult] = []
774
+ for idx in top_indices:
775
+ path = paths[idx]
776
+ score = similarities[idx]
777
+ chunk_meta = chunk_entries[idx] if idx < len(chunk_entries) else {}
778
+ start_line = chunk_meta.get("start_line")
779
+ end_line = chunk_meta.get("end_line")
780
+ scored.append(
781
+ SearchResult(
782
+ path=path,
783
+ score=float(score),
784
+ preview=chunk_meta.get("preview"),
785
+ chunk_index=int(chunk_meta.get("chunk_index", 0)),
786
+ start_line=int(start_line) if start_line is not None else None,
787
+ end_line=int(end_line) if end_line is not None else None,
788
+ )
789
+ )
790
+ if use_rerank:
791
+ candidates = scored
602
792
  if rerank == "bm25":
603
793
  candidates = _apply_bm25_rerank(request.query, candidates)
604
794
  reranker = "bm25"
@@ -623,7 +813,7 @@ def perform_search(request: SearchRequest) -> SearchResponse:
623
813
  base_path=request.directory,
624
814
  backend=searcher.device,
625
815
  results=results,
626
- is_stale=stale,
816
+ is_stale=False,
627
817
  index_empty=False,
628
818
  reranker=reranker,
629
819
  )
@@ -764,6 +954,7 @@ def _filter_index_by_extensions(
764
954
  ext_set = {ext.lower() for ext in extensions if ext}
765
955
  if not ext_set:
766
956
  return list(paths), file_vectors, metadata
957
+ chunk_ids = metadata.get("chunk_ids")
767
958
  keep_indices: list[int] = []
768
959
  filtered_paths: list[Path] = []
769
960
  for idx, path in enumerate(paths):
@@ -778,6 +969,8 @@ def _filter_index_by_extensions(
778
969
  ext_set,
779
970
  )
780
971
  filtered_metadata["chunks"] = []
972
+ if chunk_ids is not None:
973
+ filtered_metadata["chunk_ids"] = []
781
974
  return [], filtered_vectors, filtered_metadata
782
975
  filtered_vectors = file_vectors[keep_indices]
783
976
  chunk_entries = metadata.get("chunks", [])
@@ -790,6 +983,10 @@ def _filter_index_by_extensions(
790
983
  ext_set,
791
984
  )
792
985
  filtered_metadata["chunks"] = filtered_chunks
986
+ if chunk_ids is not None:
987
+ filtered_metadata["chunk_ids"] = [
988
+ chunk_ids[idx] for idx in keep_indices if idx < len(chunk_ids)
989
+ ]
793
990
  return filtered_paths, filtered_vectors, filtered_metadata
794
991
 
795
992
 
@@ -802,6 +999,7 @@ def _filter_index_by_exclude_patterns(
802
999
  ) -> tuple[list[Path], Sequence[Sequence[float]], dict]:
803
1000
  if exclude_spec is None:
804
1001
  return list(paths), file_vectors, metadata
1002
+ chunk_ids = metadata.get("chunk_ids")
805
1003
  keep_indices: list[int] = []
806
1004
  filtered_paths: list[Path] = []
807
1005
  root_resolved = root.resolve()
@@ -822,6 +1020,8 @@ def _filter_index_by_exclude_patterns(
822
1020
  exclude_spec,
823
1021
  )
824
1022
  filtered_metadata["chunks"] = []
1023
+ if chunk_ids is not None:
1024
+ filtered_metadata["chunk_ids"] = []
825
1025
  return [], filtered_vectors, filtered_metadata
826
1026
  filtered_vectors = file_vectors[keep_indices]
827
1027
  chunk_entries = metadata.get("chunks", [])
@@ -834,6 +1034,10 @@ def _filter_index_by_exclude_patterns(
834
1034
  exclude_spec,
835
1035
  )
836
1036
  filtered_metadata["chunks"] = filtered_chunks
1037
+ if chunk_ids is not None:
1038
+ filtered_metadata["chunk_ids"] = [
1039
+ chunk_ids[idx] for idx in keep_indices if idx < len(chunk_ids)
1040
+ ]
837
1041
  return filtered_paths, filtered_vectors, filtered_metadata
838
1042
 
839
1043
 
@@ -850,6 +1054,7 @@ def _filter_index_by_directory(
850
1054
  relative_dir = directory.resolve().relative_to(index_root.resolve())
851
1055
  except ValueError:
852
1056
  return list(paths), file_vectors, metadata
1057
+ chunk_ids = metadata.get("chunk_ids")
853
1058
  keep_indices: list[int] = []
854
1059
  filtered_paths: list[Path] = []
855
1060
  for idx, path in enumerate(paths):
@@ -870,6 +1075,8 @@ def _filter_index_by_directory(
870
1075
  recursive=recursive,
871
1076
  )
872
1077
  filtered_metadata["chunks"] = []
1078
+ if chunk_ids is not None:
1079
+ filtered_metadata["chunk_ids"] = []
873
1080
  filtered_metadata["root"] = str(directory)
874
1081
  return [], filtered_vectors, filtered_metadata
875
1082
  filtered_vectors = file_vectors[keep_indices]
@@ -884,6 +1091,10 @@ def _filter_index_by_directory(
884
1091
  recursive=recursive,
885
1092
  )
886
1093
  filtered_metadata["chunks"] = filtered_chunks
1094
+ if chunk_ids is not None:
1095
+ filtered_metadata["chunk_ids"] = [
1096
+ chunk_ids[idx] for idx in keep_indices if idx < len(chunk_ids)
1097
+ ]
887
1098
  filtered_metadata["root"] = str(directory)
888
1099
  return filtered_paths, filtered_vectors, filtered_metadata
889
1100
 
vexor/text.py CHANGED
@@ -19,6 +19,7 @@ class Messages:
19
19
  HELP_SEARCH_FORMAT = (
20
20
  "Output format (rich=table, porcelain=tab-separated for scripts, porcelain-z=NUL-delimited)."
21
21
  )
22
+ HELP_NO_CACHE = "Disable all disk caches (index + embedding/query)."
22
23
  HELP_INCLUDE_HIDDEN = "Use the index built with hidden files included."
23
24
  HELP_INDEX_PATH = "Root directory to scan for indexing."
24
25
  HELP_INDEX_INCLUDE = "Include hidden files and directories when building the index."
@@ -58,6 +59,8 @@ class Messages:
58
59
  HELP_SET_MODEL = "Set the default embedding model."
59
60
  HELP_SET_BATCH = "Set the default batch size (0 = single request)."
60
61
  HELP_SET_EMBED_CONCURRENCY = "Set the number of concurrent embedding requests."
62
+ HELP_SET_EXTRACT_CONCURRENCY = "Set the number of concurrent file extraction workers."
63
+ HELP_SET_EXTRACT_BACKEND = "Set the extraction backend (auto, thread, process)."
61
64
  HELP_SET_PROVIDER = "Set the default embedding provider (e.g., gemini, openai, custom, or local)."
62
65
  HELP_SET_BASE_URL = "Override the provider's base URL (leave unset for official endpoints)."
63
66
  HELP_CLEAR_BASE_URL = "Remove the custom base URL override."
@@ -116,6 +119,10 @@ class Messages:
116
119
  ERROR_EMPTY_QUERY = "Query text must not be empty."
117
120
  ERROR_BATCH_NEGATIVE = "Batch size must be >= 0"
118
121
  ERROR_CONCURRENCY_INVALID = "Embedding concurrency must be >= 1"
122
+ ERROR_EXTRACT_CONCURRENCY_INVALID = "Extraction concurrency must be >= 1"
123
+ ERROR_EXTRACT_BACKEND_INVALID = (
124
+ "Unsupported extraction backend '{value}'. Allowed values: {allowed}."
125
+ )
119
126
  ERROR_MODE_INVALID = "Unsupported mode '{value}'. Allowed values: {allowed}."
120
127
  ERROR_PROVIDER_INVALID = "Unsupported provider '{value}'. Allowed values: {allowed}."
121
128
  ERROR_RERANK_INVALID = "Unsupported rerank value '{value}'. Allowed values: {allowed}."
@@ -265,6 +272,8 @@ class Messages:
265
272
  INFO_MODEL_SET = "Default model set to {value}."
266
273
  INFO_BATCH_SET = "Default batch size set to {value}."
267
274
  INFO_EMBED_CONCURRENCY_SET = "Embedding concurrency set to {value}."
275
+ INFO_EXTRACT_CONCURRENCY_SET = "Extraction concurrency set to {value}."
276
+ INFO_EXTRACT_BACKEND_SET = "Extraction backend set to {value}."
268
277
  INFO_PROVIDER_SET = "Default provider set to {value}."
269
278
  INFO_BASE_URL_SET = "Base URL override set to {value}."
270
279
  INFO_BASE_URL_CLEARED = "Base URL override cleared."
@@ -299,12 +308,16 @@ class Messages:
299
308
  ERROR_CONFIG_EDITOR_NOT_FOUND = "Unable to determine a text editor. Set $VISUAL or $EDITOR, or install nano/vi."
300
309
  ERROR_CONFIG_EDITOR_FAILED = "Editor exited with status {code}."
301
310
  ERROR_CONFIG_EDITOR_LAUNCH = "Failed to launch editor: {reason}."
311
+ ERROR_CONFIG_JSON_INVALID = "Config JSON must be an object."
312
+ ERROR_CONFIG_VALUE_INVALID = "Config JSON has invalid value for {field}."
302
313
  INFO_CONFIG_SUMMARY = (
303
314
  "API key set: {api}\n"
304
315
  "Default provider: {provider}\n"
305
316
  "Default model: {model}\n"
306
317
  "Default batch size: {batch}\n"
307
318
  "Embedding concurrency: {concurrency}\n"
319
+ "Extract concurrency: {extract_concurrency}\n"
320
+ "Extract backend: {extract_backend}\n"
308
321
  "Auto index: {auto_index}\n"
309
322
  "Rerank: {rerank}\n"
310
323
  "{flashrank_line}"
@@ -315,6 +328,7 @@ class Messages:
315
328
  INFO_FLASHRANK_MODEL_SUMMARY = "FlashRank model: {value}"
316
329
  INFO_REMOTE_RERANK_SUMMARY = "Remote rerank: {value}"
317
330
  INFO_SEARCH_RUNNING = "Searching cached index under {path}..."
331
+ INFO_SEARCH_RUNNING_NO_CACHE = "Searching in-memory index under {path}..."
318
332
  INFO_DOCTOR_CHECKING = "Checking if `vexor` is on PATH..."
319
333
  INFO_DOCTOR_FOUND = "`vexor` command is available at {path}."
320
334
  ERROR_DOCTOR_MISSING = (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vexor
3
- Version: 0.19.0a1
3
+ Version: 0.21.0
4
4
  Summary: A vector-powered CLI for semantic search over files.
5
5
  Project-URL: Repository, https://github.com/scarletkc/vexor
6
6
  Author: scarletkc
@@ -69,9 +69,8 @@ Description-Content-Type: text/markdown
69
69
 
70
70
  ---
71
71
 
72
- **Vexor** is a vector-powered CLI and desktop app for semantic file search. It uses configurable embedding models and ranks results by cosine similarity.
73
-
74
- ![GUI](https://raw.githubusercontent.com/scarletkc/vexor/refs/heads/main/assets/gui_demo.png)
72
+ **Vexor** is a semantic search engine that builds reusable indexes over files and code.
73
+ It supports configurable embedding and reranking providers, and exposes the same core through a Python API, a CLI tool, and an optional desktop frontend.
75
74
 
76
75
  <video src="https://github.com/user-attachments/assets/4d53eefd-ab35-4232-98a7-f8dc005983a9" controls="controls" style="max-width: 600px;">
77
76
  Vexor Demo Video
@@ -98,18 +97,13 @@ vexor init
98
97
  ```
99
98
  The wizard also runs automatically on first use when no config exists.
100
99
 
101
- ### 1. Configure API Key
102
- ```bash
103
- vexor config --set-api-key "YOUR_KEY"
104
- ```
105
- Or via environment: `VEXOR_API_KEY`, `OPENAI_API_KEY`, or `GOOGLE_GENAI_API_KEY`.
106
-
107
- ### 2. Search
100
+ ### 1. Search
108
101
  ```bash
109
- vexor "api client config" # defaults to search
110
- vexor search "api client config" # searches current directory
102
+ vexor "api client config" # defaults to search current directory
111
103
  # or explicit path:
112
104
  vexor search "api client config" --path ~/projects/demo --top 5
105
+ # in-memory search only:
106
+ vexor search "api client config" --no-cache
113
107
  ```
114
108
 
115
109
  Vexor auto-indexes on first search. Example output:
@@ -122,7 +116,7 @@ Vexor semantic file search results
122
116
  3 0.809 ./tests/test_config_loader.py - tests for config loader
123
117
  ```
124
118
 
125
- ### 3. Explicit Index (Optional)
119
+ ### 2. Explicit Index (Optional)
126
120
  ```bash
127
121
  vexor index # indexes current directory
128
122
  # or explicit path:
@@ -130,6 +124,15 @@ vexor index --path ~/projects/demo --mode code
130
124
  ```
131
125
  Useful for CI warmup or when `auto_index` is disabled.
132
126
 
127
+ ## Desktop App (Experimental)
128
+
129
+ > The desktop app is experimental and not actively maintained.
130
+ > It may be unstable. For production use, prefer the CLI.
131
+
132
+ ![GUI](https://raw.githubusercontent.com/scarletkc/vexor/refs/heads/main/assets/gui_demo.png)
133
+
134
+ Download the desktop app from [releases](https://github.com/scarletkc/vexor/releases).
135
+
133
136
  ## Python API
134
137
 
135
138
  Vexor can also be imported and used directly from Python:
@@ -144,8 +147,19 @@ for hit in response.results:
144
147
  print(hit.path, hit.score)
145
148
  ```
146
149
 
147
- By default it reads `~/.vexor/config.json`. To ignore config and pass everything explicitly,
148
- set `use_config=False`.
150
+ By default it reads `~/.vexor/config.json`. For runtime config overrides, cache
151
+ controls, and per-call options, see [`docs/api/python.md`](https://github.com/scarletkc/vexor/tree/main/docs/api/python.md).
152
+
153
+ ## AI Agent Skill
154
+
155
+ This repo includes a skill for AI agents to use Vexor effectively:
156
+
157
+ ```bash
158
+ vexor install --skills claude # Claude Code
159
+ vexor install --skills codex # Codex
160
+ ```
161
+
162
+ Skill source: [`plugins/vexor/skills/vexor-cli`](https://github.com/scarletkc/vexor/raw/refs/heads/main/plugins/vexor/skills/vexor-cli/SKILL.md)
149
163
 
150
164
  ## Configuration
151
165
 
@@ -153,7 +167,9 @@ set `use_config=False`.
153
167
  vexor config --set-provider openai # default; also supports gemini/custom/local
154
168
  vexor config --set-model text-embedding-3-small
155
169
  vexor config --set-batch-size 0 # 0 = single request
156
- vexor config --set-embed-concurrency 2 # parallel embedding requests
170
+ vexor config --set-embed-concurrency 4 # parallel embedding requests
171
+ vexor config --set-extract-concurrency 4 # parallel file extraction workers
172
+ vexor config --set-extract-backend auto # auto|thread|process (default: auto)
157
173
  vexor config --set-auto-index true # auto-index before search (default)
158
174
  vexor config --rerank bm25 # optional BM25 rerank for top-k results
159
175
  vexor config --rerank flashrank # FlashRank rerank (requires optional extra)
@@ -175,10 +191,16 @@ FlashRank requires `pip install "vexor[flashrank]"` and caches models under `~/.
175
191
 
176
192
  Config stored in `~/.vexor/config.json`.
177
193
 
194
+ ### Configure API Key
195
+ ```bash
196
+ vexor config --set-api-key "YOUR_KEY"
197
+ ```
198
+ Or via environment: `VEXOR_API_KEY`, `OPENAI_API_KEY`, or `GOOGLE_GENAI_API_KEY`.
199
+
178
200
  ### Rerank
179
201
 
180
- Rerank reorders the semantic results with a secondary ranker. It uses 2x the requested
181
- `--top` as candidates (e.g., top 10 reranked to show 5).
202
+ Rerank reorders the semantic results with a secondary ranker. Candidate sizing uses
203
+ `clamp(int(--top * 2), 20, 150)`.
182
204
 
183
205
  Recommended defaults:
184
206
  - Keep `off` unless you want extra precision.
@@ -285,20 +307,10 @@ Re-running `vexor index` only re-embeds changed files; >50% changes trigger full
285
307
  | `--no-respect-gitignore` | Include gitignored files |
286
308
  | `--format porcelain` | Script-friendly TSV output |
287
309
  | `--format porcelain-z` | NUL-delimited output |
310
+ | `--no-cache` | In-memory only; do not read/write index cache |
288
311
 
289
312
  Porcelain output fields: `rank`, `similarity`, `path`, `chunk_index`, `start_line`, `end_line`, `preview` (line fields are `-` when unavailable).
290
313
 
291
- ## AI Agent Skill
292
-
293
- This repo includes a skill for AI agents to use Vexor effectively:
294
-
295
- ```bash
296
- vexor install --skills claude # Claude Code
297
- vexor install --skills codex # Codex
298
- ```
299
-
300
- Skill source: [`plugins/vexor/skills/vexor-cli`](https://github.com/scarletkc/vexor/raw/refs/heads/main/plugins/vexor/skills/vexor-cli/SKILL.md)
301
-
302
314
  ## Documentation
303
315
 
304
316
  See [docs](https://github.com/scarletkc/vexor/tree/main/docs) for more details.
@@ -0,0 +1,33 @@
1
+ vexor/__init__.py,sha256=i0ly8cFA4N_PEQ_rhYgoLp2NPRQc3_ln8Gfi8QWjXSQ,441
2
+ vexor/__main__.py,sha256=ZFzom1wCfP6TPXe3aoDFpNcUgjbCZ7Quy_vfzNsH5Fw,426
3
+ vexor/api.py,sha256=YCHpiydbPbRJUqdQYrpwe1JrRI-w_7LRuyZDGBP1_d4,11506
4
+ vexor/cache.py,sha256=3i9FKFLSyZ1kx-w1apc12umPaQxWqMP-P8_lvo67hBw,52832
5
+ vexor/cli.py,sha256=M9GKdD_mJ068Zpm62znTp0KhhKp1dkh_WHmfJHR9hwU,68094
6
+ vexor/config.py,sha256=CiPfEH7Ilt6XepEx4p02qfW5HfkpNDBjhEMyckbSWaA,17413
7
+ vexor/modes.py,sha256=N_wAWoqbxmCfko-v520p59tpAYvUwraCSSQRtMaF4ac,11549
8
+ vexor/output.py,sha256=iooZgLlK8dh7ajJ4XMHUNNx0qyTVtD_OAAwrBx5MeqE,864
9
+ vexor/search.py,sha256=MSU4RmH6waFYOofkIdo8_ElTiz1oNaKuvr-3umif7Bs,6826
10
+ vexor/text.py,sha256=2aK5nJHkosmbmyzp9o_Tzb3YlmVnju_IX8BcEPUdhTA,24794
11
+ vexor/utils.py,sha256=GzfYW2rz1-EuJjkevqZVe8flLRtrQ60OWMmFNbMh62k,12472
12
+ vexor/providers/__init__.py,sha256=kCEoV03TSLKcxDUYVNjXnrVoLU5NpfNXjp1w1Ak2imE,92
13
+ vexor/providers/gemini.py,sha256=IWHHjCMJC0hUHQPhuaJ_L_97c_mnOXkPkCVdrIR6z-g,5705
14
+ vexor/providers/local.py,sha256=5X_WYCXgyBGIVvvVLgMnDjTkPR4GBF0ksNPyviBlB7w,4838
15
+ vexor/providers/openai.py,sha256=YnJDY9gJW7RfGGdkgswVHvmOKNvgLRQUsbpA1MUuLPg,5356
16
+ vexor/services/__init__.py,sha256=dA_i2N03vlYmbZbEK2knzJLWviunkNWbzN2LWPNvMk0,160
17
+ vexor/services/cache_service.py,sha256=ywt6AgupCJ7_wC3je4znCMw5_VBouw3skbDTAt8xw6o,1639
18
+ vexor/services/config_service.py,sha256=PojolfbSKh9pW8slF4qxCOs9hz5L6xvjf_nB7vfVlsU,5039
19
+ vexor/services/content_extract_service.py,sha256=zdhLxpNv70BU7irLf3Uc0ou9rKSvdjtrDcHkgRKlMn4,26421
20
+ vexor/services/index_service.py,sha256=FXf1bBoqj4-K1l38ItxHf6Oh7QHVIdNAdVY2kg_Zoq8,32265
21
+ vexor/services/init_service.py,sha256=3D04hylGA9FRQhLHCfR95nMko3vb5MNBcRb9nWWaUE8,26863
22
+ vexor/services/js_parser.py,sha256=eRtW6KlK4JBYDGbyoecHVqLZ0hcx-Cc0kx6bOujHPAQ,16254
23
+ vexor/services/keyword_service.py,sha256=vmke8tII9kTwRDdBaLHBc6Hpy_B3p98L65iGkCQgtMU,2211
24
+ vexor/services/search_service.py,sha256=K7SiAuMA7bGeyPWOHPMKpFFvzzkj5kHWwa3p94NakJs,38663
25
+ vexor/services/skill_service.py,sha256=Rrgt3OMsKPPiXOiRhSNAWjBM9UNz9qmSWQe3uYGzq4M,4863
26
+ vexor/services/system_service.py,sha256=KPlv83v3rTvBiNiH7vrp6tDmt_AqHxuUd-5RI0TfvWs,24638
27
+ vexor/_bundled_skills/vexor-cli/SKILL.md,sha256=m3FlyqgHBdRwyGPEp8PrUS21K0G2jEl88tRvhSPta08,2798
28
+ vexor/_bundled_skills/vexor-cli/references/install-vexor.md,sha256=IUBShLI1mAxugwUIMAJQ5_j6KcaPWfobe0gSd6MWU7w,1245
29
+ vexor-0.21.0.dist-info/METADATA,sha256=Lc5PHY_Ir3F56ILYe6IBlkwhN6gMQGZvf48f7x_uVDg,13494
30
+ vexor-0.21.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
+ vexor-0.21.0.dist-info/entry_points.txt,sha256=dvxp6Q1R1d6bozR7TwmpdJ0X_v83MkzsLPagGY_lfr0,40
32
+ vexor-0.21.0.dist-info/licenses/LICENSE,sha256=wP7TAKRll1t9LoYGxWS9NikPM_0hCc00LmlLyvQBsL8,1066
33
+ vexor-0.21.0.dist-info/RECORD,,