exonware-xwnode 0.0.1.21__py3-none-any.whl → 0.0.1.23__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.
- exonware/__init__.py +8 -1
- exonware/xwnode/__init__.py +18 -5
- exonware/xwnode/add_strategy_types.py +165 -0
- exonware/xwnode/base.py +7 -5
- exonware/xwnode/common/__init__.py +1 -1
- exonware/xwnode/common/graph/__init__.py +30 -0
- exonware/xwnode/common/graph/caching.py +131 -0
- exonware/xwnode/common/graph/contracts.py +100 -0
- exonware/xwnode/common/graph/errors.py +44 -0
- exonware/xwnode/common/graph/indexing.py +260 -0
- exonware/xwnode/common/graph/manager.py +568 -0
- exonware/xwnode/common/management/__init__.py +3 -5
- exonware/xwnode/common/management/manager.py +9 -9
- exonware/xwnode/common/management/migration.py +6 -6
- exonware/xwnode/common/monitoring/__init__.py +3 -5
- exonware/xwnode/common/monitoring/metrics.py +7 -3
- exonware/xwnode/common/monitoring/pattern_detector.py +2 -2
- exonware/xwnode/common/monitoring/performance_monitor.py +6 -2
- exonware/xwnode/common/patterns/__init__.py +3 -5
- exonware/xwnode/common/patterns/advisor.py +1 -1
- exonware/xwnode/common/patterns/flyweight.py +6 -2
- exonware/xwnode/common/patterns/registry.py +203 -184
- exonware/xwnode/common/utils/__init__.py +25 -11
- exonware/xwnode/common/utils/simple.py +1 -1
- exonware/xwnode/config.py +3 -8
- exonware/xwnode/contracts.py +4 -105
- exonware/xwnode/defs.py +413 -159
- exonware/xwnode/edges/strategies/__init__.py +86 -4
- exonware/xwnode/edges/strategies/_base_edge.py +2 -2
- exonware/xwnode/edges/strategies/adj_list.py +287 -121
- exonware/xwnode/edges/strategies/adj_matrix.py +316 -222
- exonware/xwnode/edges/strategies/base.py +1 -1
- exonware/xwnode/edges/strategies/{edge_bidir_wrapper.py → bidir_wrapper.py} +45 -4
- exonware/xwnode/edges/strategies/bitemporal.py +520 -0
- exonware/xwnode/edges/strategies/{edge_block_adj_matrix.py → block_adj_matrix.py} +77 -6
- exonware/xwnode/edges/strategies/bv_graph.py +664 -0
- exonware/xwnode/edges/strategies/compressed_graph.py +217 -0
- exonware/xwnode/edges/strategies/{edge_coo.py → coo.py} +46 -4
- exonware/xwnode/edges/strategies/{edge_csc.py → csc.py} +45 -4
- exonware/xwnode/edges/strategies/{edge_csr.py → csr.py} +94 -12
- exonware/xwnode/edges/strategies/{edge_dynamic_adj_list.py → dynamic_adj_list.py} +46 -4
- exonware/xwnode/edges/strategies/edge_list.py +168 -0
- exonware/xwnode/edges/strategies/edge_property_store.py +2 -2
- exonware/xwnode/edges/strategies/euler_tour.py +560 -0
- exonware/xwnode/edges/strategies/{edge_flow_network.py → flow_network.py} +2 -2
- exonware/xwnode/edges/strategies/graphblas.py +449 -0
- exonware/xwnode/edges/strategies/hnsw.py +637 -0
- exonware/xwnode/edges/strategies/hop2_labels.py +467 -0
- exonware/xwnode/edges/strategies/{edge_hyperedge_set.py → hyperedge_set.py} +2 -2
- exonware/xwnode/edges/strategies/incidence_matrix.py +250 -0
- exonware/xwnode/edges/strategies/k2_tree.py +613 -0
- exonware/xwnode/edges/strategies/link_cut.py +626 -0
- exonware/xwnode/edges/strategies/multiplex.py +532 -0
- exonware/xwnode/edges/strategies/{edge_neural_graph.py → neural_graph.py} +2 -2
- exonware/xwnode/edges/strategies/{edge_octree.py → octree.py} +69 -11
- exonware/xwnode/edges/strategies/{edge_quadtree.py → quadtree.py} +66 -10
- exonware/xwnode/edges/strategies/roaring_adj.py +438 -0
- exonware/xwnode/edges/strategies/{edge_rtree.py → rtree.py} +43 -5
- exonware/xwnode/edges/strategies/{edge_temporal_edgeset.py → temporal_edgeset.py} +24 -5
- exonware/xwnode/edges/strategies/{edge_tree_graph_basic.py → tree_graph_basic.py} +78 -7
- exonware/xwnode/edges/strategies/{edge_weighted_graph.py → weighted_graph.py} +188 -10
- exonware/xwnode/errors.py +3 -6
- exonware/xwnode/facade.py +20 -20
- exonware/xwnode/nodes/strategies/__init__.py +29 -9
- exonware/xwnode/nodes/strategies/adjacency_list.py +650 -177
- exonware/xwnode/nodes/strategies/aho_corasick.py +358 -183
- exonware/xwnode/nodes/strategies/array_list.py +36 -3
- exonware/xwnode/nodes/strategies/art.py +581 -0
- exonware/xwnode/nodes/strategies/{node_avl_tree.py → avl_tree.py} +77 -6
- exonware/xwnode/nodes/strategies/{node_b_plus_tree.py → b_plus_tree.py} +81 -40
- exonware/xwnode/nodes/strategies/{node_btree.py → b_tree.py} +79 -9
- exonware/xwnode/nodes/strategies/base.py +469 -98
- exonware/xwnode/nodes/strategies/{node_bitmap.py → bitmap.py} +12 -12
- exonware/xwnode/nodes/strategies/{node_bitset_dynamic.py → bitset_dynamic.py} +11 -11
- exonware/xwnode/nodes/strategies/{node_bloom_filter.py → bloom_filter.py} +15 -2
- exonware/xwnode/nodes/strategies/bloomier_filter.py +519 -0
- exonware/xwnode/nodes/strategies/bw_tree.py +531 -0
- exonware/xwnode/nodes/strategies/contracts.py +1 -1
- exonware/xwnode/nodes/strategies/{node_count_min_sketch.py → count_min_sketch.py} +3 -2
- exonware/xwnode/nodes/strategies/{node_cow_tree.py → cow_tree.py} +135 -13
- exonware/xwnode/nodes/strategies/crdt_map.py +629 -0
- exonware/xwnode/nodes/strategies/{node_cuckoo_hash.py → cuckoo_hash.py} +2 -2
- exonware/xwnode/nodes/strategies/{node_xdata_optimized.py → data_interchange_optimized.py} +21 -4
- exonware/xwnode/nodes/strategies/dawg.py +876 -0
- exonware/xwnode/nodes/strategies/deque.py +321 -153
- exonware/xwnode/nodes/strategies/extendible_hash.py +93 -0
- exonware/xwnode/nodes/strategies/{node_fenwick_tree.py → fenwick_tree.py} +111 -19
- exonware/xwnode/nodes/strategies/hamt.py +403 -0
- exonware/xwnode/nodes/strategies/hash_map.py +354 -67
- exonware/xwnode/nodes/strategies/heap.py +105 -5
- exonware/xwnode/nodes/strategies/hopscotch_hash.py +525 -0
- exonware/xwnode/nodes/strategies/{node_hyperloglog.py → hyperloglog.py} +6 -5
- exonware/xwnode/nodes/strategies/interval_tree.py +742 -0
- exonware/xwnode/nodes/strategies/kd_tree.py +703 -0
- exonware/xwnode/nodes/strategies/learned_index.py +533 -0
- exonware/xwnode/nodes/strategies/linear_hash.py +93 -0
- exonware/xwnode/nodes/strategies/linked_list.py +316 -119
- exonware/xwnode/nodes/strategies/{node_lsm_tree.py → lsm_tree.py} +219 -15
- exonware/xwnode/nodes/strategies/masstree.py +130 -0
- exonware/xwnode/nodes/strategies/{node_persistent_tree.py → persistent_tree.py} +149 -9
- exonware/xwnode/nodes/strategies/priority_queue.py +544 -132
- exonware/xwnode/nodes/strategies/queue.py +249 -120
- exonware/xwnode/nodes/strategies/{node_red_black_tree.py → red_black_tree.py} +183 -72
- exonware/xwnode/nodes/strategies/{node_roaring_bitmap.py → roaring_bitmap.py} +19 -6
- exonware/xwnode/nodes/strategies/rope.py +717 -0
- exonware/xwnode/nodes/strategies/{node_segment_tree.py → segment_tree.py} +106 -106
- exonware/xwnode/nodes/strategies/{node_set_hash.py → set_hash.py} +30 -29
- exonware/xwnode/nodes/strategies/{node_skip_list.py → skip_list.py} +74 -6
- exonware/xwnode/nodes/strategies/sparse_matrix.py +427 -131
- exonware/xwnode/nodes/strategies/{node_splay_tree.py → splay_tree.py} +55 -6
- exonware/xwnode/nodes/strategies/stack.py +244 -112
- exonware/xwnode/nodes/strategies/{node_suffix_array.py → suffix_array.py} +5 -1
- exonware/xwnode/nodes/strategies/t_tree.py +94 -0
- exonware/xwnode/nodes/strategies/{node_treap.py → treap.py} +75 -6
- exonware/xwnode/nodes/strategies/{node_tree_graph_hybrid.py → tree_graph_hybrid.py} +46 -5
- exonware/xwnode/nodes/strategies/trie.py +153 -9
- exonware/xwnode/nodes/strategies/union_find.py +111 -5
- exonware/xwnode/nodes/strategies/veb_tree.py +856 -0
- exonware/xwnode/strategies/__init__.py +5 -51
- exonware/xwnode/version.py +3 -3
- {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/METADATA +23 -3
- exonware_xwnode-0.0.1.23.dist-info/RECORD +130 -0
- exonware/xwnode/edges/strategies/edge_adj_list.py +0 -353
- exonware/xwnode/edges/strategies/edge_adj_matrix.py +0 -445
- exonware/xwnode/nodes/strategies/_base_node.py +0 -307
- exonware/xwnode/nodes/strategies/node_aho_corasick.py +0 -525
- exonware/xwnode/nodes/strategies/node_array_list.py +0 -179
- exonware/xwnode/nodes/strategies/node_hash_map.py +0 -273
- exonware/xwnode/nodes/strategies/node_heap.py +0 -196
- exonware/xwnode/nodes/strategies/node_linked_list.py +0 -413
- exonware/xwnode/nodes/strategies/node_trie.py +0 -257
- exonware/xwnode/nodes/strategies/node_union_find.py +0 -192
- exonware/xwnode/queries/executors/__init__.py +0 -47
- exonware/xwnode/queries/executors/advanced/__init__.py +0 -37
- exonware/xwnode/queries/executors/advanced/aggregate_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/ask_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/construct_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/describe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/for_loop_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/foreach_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/join_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/let_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/mutation_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/options_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/pipe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/subscribe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/subscription_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/union_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/window_executor.py +0 -51
- exonware/xwnode/queries/executors/advanced/with_cte_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/__init__.py +0 -21
- exonware/xwnode/queries/executors/aggregation/avg_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/count_executor.py +0 -38
- exonware/xwnode/queries/executors/aggregation/distinct_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/group_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/having_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/max_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/min_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/sum_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/summarize_executor.py +0 -50
- exonware/xwnode/queries/executors/array/__init__.py +0 -9
- exonware/xwnode/queries/executors/array/indexing_executor.py +0 -51
- exonware/xwnode/queries/executors/array/slicing_executor.py +0 -51
- exonware/xwnode/queries/executors/base.py +0 -257
- exonware/xwnode/queries/executors/capability_checker.py +0 -204
- exonware/xwnode/queries/executors/contracts.py +0 -166
- exonware/xwnode/queries/executors/core/__init__.py +0 -17
- exonware/xwnode/queries/executors/core/create_executor.py +0 -96
- exonware/xwnode/queries/executors/core/delete_executor.py +0 -99
- exonware/xwnode/queries/executors/core/drop_executor.py +0 -100
- exonware/xwnode/queries/executors/core/insert_executor.py +0 -39
- exonware/xwnode/queries/executors/core/select_executor.py +0 -152
- exonware/xwnode/queries/executors/core/update_executor.py +0 -102
- exonware/xwnode/queries/executors/data/__init__.py +0 -13
- exonware/xwnode/queries/executors/data/alter_executor.py +0 -50
- exonware/xwnode/queries/executors/data/load_executor.py +0 -50
- exonware/xwnode/queries/executors/data/merge_executor.py +0 -50
- exonware/xwnode/queries/executors/data/store_executor.py +0 -50
- exonware/xwnode/queries/executors/defs.py +0 -93
- exonware/xwnode/queries/executors/engine.py +0 -221
- exonware/xwnode/queries/executors/errors.py +0 -68
- exonware/xwnode/queries/executors/filtering/__init__.py +0 -25
- exonware/xwnode/queries/executors/filtering/between_executor.py +0 -80
- exonware/xwnode/queries/executors/filtering/filter_executor.py +0 -79
- exonware/xwnode/queries/executors/filtering/has_executor.py +0 -70
- exonware/xwnode/queries/executors/filtering/in_executor.py +0 -70
- exonware/xwnode/queries/executors/filtering/like_executor.py +0 -76
- exonware/xwnode/queries/executors/filtering/optional_executor.py +0 -76
- exonware/xwnode/queries/executors/filtering/range_executor.py +0 -80
- exonware/xwnode/queries/executors/filtering/term_executor.py +0 -77
- exonware/xwnode/queries/executors/filtering/values_executor.py +0 -71
- exonware/xwnode/queries/executors/filtering/where_executor.py +0 -44
- exonware/xwnode/queries/executors/graph/__init__.py +0 -15
- exonware/xwnode/queries/executors/graph/in_traverse_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/match_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/out_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/path_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/return_executor.py +0 -51
- exonware/xwnode/queries/executors/ordering/__init__.py +0 -9
- exonware/xwnode/queries/executors/ordering/by_executor.py +0 -50
- exonware/xwnode/queries/executors/ordering/order_executor.py +0 -51
- exonware/xwnode/queries/executors/projection/__init__.py +0 -9
- exonware/xwnode/queries/executors/projection/extend_executor.py +0 -50
- exonware/xwnode/queries/executors/projection/project_executor.py +0 -50
- exonware/xwnode/queries/executors/registry.py +0 -173
- exonware/xwnode/queries/parsers/__init__.py +0 -26
- exonware/xwnode/queries/parsers/base.py +0 -86
- exonware/xwnode/queries/parsers/contracts.py +0 -46
- exonware/xwnode/queries/parsers/errors.py +0 -53
- exonware/xwnode/queries/parsers/sql_param_extractor.py +0 -318
- exonware/xwnode/queries/strategies/__init__.py +0 -24
- exonware/xwnode/queries/strategies/base.py +0 -236
- exonware/xwnode/queries/strategies/cql.py +0 -201
- exonware/xwnode/queries/strategies/cypher.py +0 -181
- exonware/xwnode/queries/strategies/datalog.py +0 -70
- exonware/xwnode/queries/strategies/elastic_dsl.py +0 -70
- exonware/xwnode/queries/strategies/eql.py +0 -70
- exonware/xwnode/queries/strategies/flux.py +0 -70
- exonware/xwnode/queries/strategies/gql.py +0 -70
- exonware/xwnode/queries/strategies/graphql.py +0 -240
- exonware/xwnode/queries/strategies/gremlin.py +0 -181
- exonware/xwnode/queries/strategies/hiveql.py +0 -214
- exonware/xwnode/queries/strategies/hql.py +0 -70
- exonware/xwnode/queries/strategies/jmespath.py +0 -219
- exonware/xwnode/queries/strategies/jq.py +0 -66
- exonware/xwnode/queries/strategies/json_query.py +0 -66
- exonware/xwnode/queries/strategies/jsoniq.py +0 -248
- exonware/xwnode/queries/strategies/kql.py +0 -70
- exonware/xwnode/queries/strategies/linq.py +0 -238
- exonware/xwnode/queries/strategies/logql.py +0 -70
- exonware/xwnode/queries/strategies/mql.py +0 -68
- exonware/xwnode/queries/strategies/n1ql.py +0 -210
- exonware/xwnode/queries/strategies/partiql.py +0 -70
- exonware/xwnode/queries/strategies/pig.py +0 -215
- exonware/xwnode/queries/strategies/promql.py +0 -70
- exonware/xwnode/queries/strategies/sparql.py +0 -220
- exonware/xwnode/queries/strategies/sql.py +0 -275
- exonware/xwnode/queries/strategies/xml_query.py +0 -66
- exonware/xwnode/queries/strategies/xpath.py +0 -223
- exonware/xwnode/queries/strategies/xquery.py +0 -258
- exonware/xwnode/queries/strategies/xwnode_executor.py +0 -332
- exonware/xwnode/queries/strategies/xwquery.py +0 -456
- exonware_xwnode-0.0.1.21.dist-info/RECORD +0 -214
- /exonware/xwnode/nodes/strategies/{node_ordered_map.py → ordered_map.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_ordered_map_balanced.py → ordered_map_balanced.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_patricia.py → patricia.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_radix_trie.py → radix_trie.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_set_tree.py → set_tree.py} +0 -0
- {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/WHEEL +0 -0
- {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,637 @@
|
|
1
|
+
"""
|
2
|
+
#exonware/xwnode/src/exonware/xwnode/edges/strategies/hnsw.py
|
3
|
+
|
4
|
+
HNSW (Hierarchical Navigable Small World) Edge Strategy Implementation
|
5
|
+
|
6
|
+
This module implements the HNSW strategy for approximate nearest neighbor
|
7
|
+
search using proximity graphs with hierarchical navigation.
|
8
|
+
|
9
|
+
Company: eXonware.com
|
10
|
+
Author: Eng. Muhammad AlShehri
|
11
|
+
Email: connect@exonware.com
|
12
|
+
Version: 0.0.1.23
|
13
|
+
Generation Date: 12-Oct-2025
|
14
|
+
"""
|
15
|
+
|
16
|
+
import math
|
17
|
+
import random
|
18
|
+
from typing import Any, Iterator, Dict, List, Set, Optional, Tuple, Callable
|
19
|
+
from collections import defaultdict, deque
|
20
|
+
from ._base_edge import AEdgeStrategy
|
21
|
+
from ...defs import EdgeMode, EdgeTrait
|
22
|
+
from ...errors import XWNodeError, XWNodeValueError
|
23
|
+
|
24
|
+
|
25
|
+
class HNSWStrategy(AEdgeStrategy):
|
26
|
+
"""
|
27
|
+
HNSW (Hierarchical Navigable Small World) strategy for ANN search.
|
28
|
+
|
29
|
+
WHY HNSW:
|
30
|
+
- De-facto standard for vector similarity search
|
31
|
+
- O(log n) approximate nearest neighbor queries
|
32
|
+
- Scalable to billions of vectors
|
33
|
+
- Excellent recall with tunable accuracy/speed trade-off
|
34
|
+
- Used in production by Spotify, Pinterest, Alibaba
|
35
|
+
|
36
|
+
WHY this implementation:
|
37
|
+
- Hierarchical layers enable fast greedy routing
|
38
|
+
- Probabilistic layer assignment ensures logarithmic navigation
|
39
|
+
- M parameter controls connectivity/memory trade-off
|
40
|
+
- ef parameter controls search accuracy
|
41
|
+
- Supports custom distance metrics (Euclidean, cosine, etc.)
|
42
|
+
|
43
|
+
Time Complexity:
|
44
|
+
- Insert: O(M × log n) expected
|
45
|
+
- Search k-NN: O(ef × log n) where ef is search beam width
|
46
|
+
- Delete: O(M × log n)
|
47
|
+
- Build: O(n × M × log n) for n vectors
|
48
|
+
|
49
|
+
Space Complexity: O(n × M × log n) for n vectors
|
50
|
+
|
51
|
+
Trade-offs:
|
52
|
+
- Advantage: State-of-the-art recall/speed trade-off
|
53
|
+
- Advantage: Scales to billions of vectors
|
54
|
+
- Advantage: Fast incremental updates (no retraining)
|
55
|
+
- Limitation: Approximate results (tunable accuracy)
|
56
|
+
- Limitation: Higher memory than IVF/PQ methods
|
57
|
+
- Limitation: Requires parameter tuning (M, ef, ef_construction)
|
58
|
+
- Compared to Annoy: Better recall, more memory
|
59
|
+
- Compared to FAISS IVF: Better recall, slower build
|
60
|
+
|
61
|
+
Best for:
|
62
|
+
- Vector similarity search (embeddings, images, audio)
|
63
|
+
- Recommendation systems
|
64
|
+
- Semantic search
|
65
|
+
- Image retrieval
|
66
|
+
- Document similarity
|
67
|
+
- Any high-dimensional ANN queries
|
68
|
+
|
69
|
+
Not recommended for:
|
70
|
+
- Exact nearest neighbor (use k-d tree for low dimensions)
|
71
|
+
- Extremely high dimensions (>1000) without dimension reduction
|
72
|
+
- Memory-constrained environments (use PQ compression)
|
73
|
+
- When perfect recall required
|
74
|
+
- Very small datasets (<1000 vectors)
|
75
|
+
|
76
|
+
Following eXonware Priorities:
|
77
|
+
1. Security: Validates vectors, prevents malformed graphs
|
78
|
+
2. Usability: Simple add/search API, standard metrics
|
79
|
+
3. Maintainability: Clean layer structure, well-documented
|
80
|
+
4. Performance: O(log n) search, highly optimized
|
81
|
+
5. Extensibility: Configurable metrics, parameters, pruning
|
82
|
+
|
83
|
+
Industry Best Practices:
|
84
|
+
- Follows Malkov & Yashunin HNSW paper (2016)
|
85
|
+
- Uses M=16, ef_construction=200 as defaults
|
86
|
+
- Implements heuristic for layer selection (ml=1/ln(2))
|
87
|
+
- Provides greedy search with ef beam
|
88
|
+
- Compatible with FAISS, Annoy, nmslib
|
89
|
+
"""
|
90
|
+
|
91
|
+
def __init__(self, traits: EdgeTrait = EdgeTrait.NONE,
|
92
|
+
M: int = 16,
|
93
|
+
M_max: int = 16,
|
94
|
+
ef_construction: int = 200,
|
95
|
+
ml: float = 1.0 / math.log(2.0),
|
96
|
+
distance_metric: str = "euclidean", **options):
|
97
|
+
"""
|
98
|
+
Initialize HNSW strategy.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
traits: Edge traits
|
102
|
+
M: Number of connections per element
|
103
|
+
M_max: Maximum connections per element
|
104
|
+
ef_construction: Size of dynamic candidate list during construction
|
105
|
+
ml: Normalization factor for level assignment
|
106
|
+
distance_metric: Distance metric (euclidean, cosine, manhattan)
|
107
|
+
**options: Additional options
|
108
|
+
"""
|
109
|
+
super().__init__(EdgeMode.HNSW, traits, **options)
|
110
|
+
|
111
|
+
self.M = M
|
112
|
+
self.M_max = M_max
|
113
|
+
self.M_max_0 = M_max * 2 # Level 0 can have more connections
|
114
|
+
self.ef_construction = ef_construction
|
115
|
+
self.ml = ml
|
116
|
+
self.distance_metric = distance_metric
|
117
|
+
|
118
|
+
# Multi-layer graph structure
|
119
|
+
# _layers[vertex][layer] = set of neighbors at that layer
|
120
|
+
self._layers: Dict[str, Dict[int, Set[str]]] = defaultdict(lambda: defaultdict(set))
|
121
|
+
|
122
|
+
# Vector storage
|
123
|
+
self._vectors: Dict[str, Tuple[float, ...]] = {}
|
124
|
+
|
125
|
+
# Entry point (highest layer vertex)
|
126
|
+
self._entry_point: Optional[str] = None
|
127
|
+
self._entry_layer = -1
|
128
|
+
|
129
|
+
# Track vertices
|
130
|
+
self._vertices: Set[str] = set()
|
131
|
+
|
132
|
+
def get_supported_traits(self) -> EdgeTrait:
|
133
|
+
"""Get supported traits."""
|
134
|
+
return EdgeTrait.SPARSE | EdgeTrait.MULTI | EdgeTrait.DIRECTED
|
135
|
+
|
136
|
+
# ============================================================================
|
137
|
+
# DISTANCE METRICS
|
138
|
+
# ============================================================================
|
139
|
+
|
140
|
+
def _distance(self, v1: Tuple[float, ...], v2: Tuple[float, ...]) -> float:
|
141
|
+
"""
|
142
|
+
Calculate distance between vectors.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
v1: First vector
|
146
|
+
v2: Second vector
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
Distance
|
150
|
+
|
151
|
+
WHY configurable metrics:
|
152
|
+
- Different data types need different metrics
|
153
|
+
- Euclidean for general use
|
154
|
+
- Cosine for text embeddings
|
155
|
+
- Manhattan for categorical data
|
156
|
+
"""
|
157
|
+
if self.distance_metric == "euclidean":
|
158
|
+
return math.sqrt(sum((a - b) ** 2 for a, b in zip(v1, v2)))
|
159
|
+
elif self.distance_metric == "cosine":
|
160
|
+
dot = sum(a * b for a, b in zip(v1, v2))
|
161
|
+
norm1 = math.sqrt(sum(a ** 2 for a in v1))
|
162
|
+
norm2 = math.sqrt(sum(b ** 2 for b in v2))
|
163
|
+
return 1.0 - (dot / (norm1 * norm2)) if norm1 * norm2 > 0 else 1.0
|
164
|
+
elif self.distance_metric == "manhattan":
|
165
|
+
return sum(abs(a - b) for a, b in zip(v1, v2))
|
166
|
+
else:
|
167
|
+
raise XWNodeValueError(f"Unknown distance metric: {self.distance_metric}")
|
168
|
+
|
169
|
+
# ============================================================================
|
170
|
+
# LAYER ASSIGNMENT
|
171
|
+
# ============================================================================
|
172
|
+
|
173
|
+
def _select_layer(self) -> int:
|
174
|
+
"""
|
175
|
+
Select layer for new element.
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
Layer number
|
179
|
+
|
180
|
+
WHY probabilistic layers:
|
181
|
+
- Creates skip-list-like structure
|
182
|
+
- Ensures O(log n) expected navigation
|
183
|
+
- ml=1/ln(2) is theoretically optimal
|
184
|
+
"""
|
185
|
+
return int(-math.log(random.uniform(0, 1)) * self.ml)
|
186
|
+
|
187
|
+
# ============================================================================
|
188
|
+
# CORE HNSW OPERATIONS
|
189
|
+
# ============================================================================
|
190
|
+
|
191
|
+
def add_vector(self, vertex: str, vector: Tuple[float, ...]) -> None:
|
192
|
+
"""
|
193
|
+
Add vector with HNSW index construction.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
vertex: Vertex identifier
|
197
|
+
vector: Vector coordinates
|
198
|
+
|
199
|
+
Raises:
|
200
|
+
XWNodeValueError: If vertex already exists
|
201
|
+
|
202
|
+
WHY greedy insertion:
|
203
|
+
- Finds nearest neighbors in each layer
|
204
|
+
- Connects to M closest at each level
|
205
|
+
- Maintains navigability property
|
206
|
+
"""
|
207
|
+
if vertex in self._vectors:
|
208
|
+
raise XWNodeValueError(f"Vertex '{vertex}' already exists")
|
209
|
+
|
210
|
+
self._vectors[vertex] = vector
|
211
|
+
self._vertices.add(vertex)
|
212
|
+
|
213
|
+
# Select layer for new element
|
214
|
+
layer = self._select_layer()
|
215
|
+
|
216
|
+
# Update entry point if necessary
|
217
|
+
if layer > self._entry_layer:
|
218
|
+
self._entry_point = vertex
|
219
|
+
self._entry_layer = layer
|
220
|
+
|
221
|
+
# Search for nearest neighbors
|
222
|
+
if self._entry_point and self._entry_point != vertex:
|
223
|
+
nearest = self._search_layer(vector, self._entry_point, 1, layer + 1)
|
224
|
+
|
225
|
+
if nearest:
|
226
|
+
ep = nearest[0][1] # Closest vertex
|
227
|
+
|
228
|
+
# Insert into each layer
|
229
|
+
for lc in range(layer, -1, -1):
|
230
|
+
candidates = self._search_layer(vector, ep, self.ef_construction, lc)
|
231
|
+
|
232
|
+
# Select M neighbors
|
233
|
+
M = self.M_max_0 if lc == 0 else self.M_max
|
234
|
+
neighbors = self._get_neighbors_heuristic(vertex, candidates, M)
|
235
|
+
|
236
|
+
# Add bidirectional links
|
237
|
+
for neighbor in neighbors:
|
238
|
+
self._layers[vertex][lc].add(neighbor)
|
239
|
+
self._layers[neighbor][lc].add(vertex)
|
240
|
+
|
241
|
+
# Prune neighbor connections if needed
|
242
|
+
M_max = self.M_max_0 if lc == 0 else self.M_max
|
243
|
+
if len(self._layers[neighbor][lc]) > M_max:
|
244
|
+
self._prune_connections(neighbor, lc, M_max)
|
245
|
+
|
246
|
+
self._edge_count += sum(len(neighbors) for neighbors in self._layers[vertex].values())
|
247
|
+
|
248
|
+
def _search_layer(self, query: Tuple[float, ...], entry_point: str,
|
249
|
+
ef: int, layer: int) -> List[Tuple[float, str]]:
|
250
|
+
"""
|
251
|
+
Search for nearest neighbors in layer.
|
252
|
+
|
253
|
+
Args:
|
254
|
+
query: Query vector
|
255
|
+
entry_point: Starting vertex
|
256
|
+
ef: Size of dynamic candidate list
|
257
|
+
layer: Layer to search
|
258
|
+
|
259
|
+
Returns:
|
260
|
+
List of (distance, vertex) tuples
|
261
|
+
|
262
|
+
WHY greedy search:
|
263
|
+
- Navigates to local minimum
|
264
|
+
- Uses ef candidates for broader exploration
|
265
|
+
- Balances accuracy and speed
|
266
|
+
"""
|
267
|
+
visited = {entry_point}
|
268
|
+
candidates = [(self._distance(query, self._vectors[entry_point]), entry_point)]
|
269
|
+
w = candidates.copy()
|
270
|
+
|
271
|
+
while candidates:
|
272
|
+
# Get closest unvisited candidate
|
273
|
+
candidates.sort()
|
274
|
+
c_dist, c = candidates.pop(0)
|
275
|
+
|
276
|
+
# Get furthest in result set
|
277
|
+
f_dist = w[-1][0] if w else float('inf')
|
278
|
+
|
279
|
+
if c_dist > f_dist:
|
280
|
+
break
|
281
|
+
|
282
|
+
# Explore neighbors
|
283
|
+
for neighbor in self._layers[c].get(layer, []):
|
284
|
+
if neighbor not in visited:
|
285
|
+
visited.add(neighbor)
|
286
|
+
f_dist = w[-1][0] if len(w) >= ef else float('inf')
|
287
|
+
d = self._distance(query, self._vectors[neighbor])
|
288
|
+
|
289
|
+
if d < f_dist or len(w) < ef:
|
290
|
+
candidates.append((d, neighbor))
|
291
|
+
w.append((d, neighbor))
|
292
|
+
w.sort()
|
293
|
+
if len(w) > ef:
|
294
|
+
w.pop()
|
295
|
+
|
296
|
+
return w
|
297
|
+
|
298
|
+
def _get_neighbors_heuristic(self, vertex: str, candidates: List[Tuple[float, str]], M: int) -> List[str]:
|
299
|
+
"""
|
300
|
+
Select M neighbors using heuristic.
|
301
|
+
|
302
|
+
Args:
|
303
|
+
vertex: Current vertex
|
304
|
+
candidates: Candidate neighbors with distances
|
305
|
+
M: Number to select
|
306
|
+
|
307
|
+
Returns:
|
308
|
+
Selected neighbors
|
309
|
+
|
310
|
+
WHY heuristic:
|
311
|
+
- Simple: closest M neighbors
|
312
|
+
- Advanced: ensures connectivity
|
313
|
+
- Balances local and global optimality
|
314
|
+
"""
|
315
|
+
# Simple heuristic: select M closest
|
316
|
+
candidates.sort()
|
317
|
+
return [v for d, v in candidates[:M]]
|
318
|
+
|
319
|
+
def _prune_connections(self, vertex: str, layer: int, M_max: int) -> None:
|
320
|
+
"""
|
321
|
+
Prune connections to maintain M_max limit.
|
322
|
+
|
323
|
+
Args:
|
324
|
+
vertex: Vertex to prune
|
325
|
+
layer: Layer number
|
326
|
+
M_max: Maximum connections
|
327
|
+
"""
|
328
|
+
neighbors = list(self._layers[vertex][layer])
|
329
|
+
|
330
|
+
if len(neighbors) <= M_max:
|
331
|
+
return
|
332
|
+
|
333
|
+
# Sort by distance and keep closest M_max
|
334
|
+
vector = self._vectors[vertex]
|
335
|
+
neighbors_with_dist = [
|
336
|
+
(self._distance(vector, self._vectors[n]), n) for n in neighbors
|
337
|
+
]
|
338
|
+
neighbors_with_dist.sort()
|
339
|
+
|
340
|
+
# Keep closest M_max
|
341
|
+
kept = {n for d, n in neighbors_with_dist[:M_max]}
|
342
|
+
removed = set(neighbors) - kept
|
343
|
+
|
344
|
+
# Update connections
|
345
|
+
self._layers[vertex][layer] = kept
|
346
|
+
|
347
|
+
# Remove reverse connections
|
348
|
+
for neighbor in removed:
|
349
|
+
self._layers[neighbor][layer].discard(vertex)
|
350
|
+
|
351
|
+
# ============================================================================
|
352
|
+
# GRAPH OPERATIONS
|
353
|
+
# ============================================================================
|
354
|
+
|
355
|
+
def add_edge(self, source: str, target: str, edge_type: str = "default",
|
356
|
+
weight: float = 1.0, properties: Optional[Dict[str, Any]] = None,
|
357
|
+
is_bidirectional: bool = False, edge_id: Optional[str] = None) -> str:
|
358
|
+
"""
|
359
|
+
Add edge (requires vectors).
|
360
|
+
|
361
|
+
Note: For HNSW, use add_vector() instead.
|
362
|
+
This method is for compatibility.
|
363
|
+
"""
|
364
|
+
# Add vertices if not present (with dummy vectors)
|
365
|
+
if source not in self._vectors:
|
366
|
+
self._vertices.add(source)
|
367
|
+
if target not in self._vectors:
|
368
|
+
self._vertices.add(target)
|
369
|
+
|
370
|
+
# Add connection at layer 0
|
371
|
+
self._layers[source][0].add(target)
|
372
|
+
if is_bidirectional:
|
373
|
+
self._layers[target][0].add(source)
|
374
|
+
|
375
|
+
self._edge_count += 1
|
376
|
+
return edge_id or f"edge_{source}_{target}"
|
377
|
+
|
378
|
+
def search_knn(self, query: Tuple[float, ...], k: int, ef: Optional[int] = None) -> List[Tuple[str, float]]:
|
379
|
+
"""
|
380
|
+
Search for k nearest neighbors.
|
381
|
+
|
382
|
+
Args:
|
383
|
+
query: Query vector
|
384
|
+
k: Number of neighbors to return
|
385
|
+
ef: Search parameter (larger = more accurate)
|
386
|
+
|
387
|
+
Returns:
|
388
|
+
List of (vertex, distance) tuples
|
389
|
+
|
390
|
+
Raises:
|
391
|
+
XWNodeValueError: If k < 1 or no entry point
|
392
|
+
|
393
|
+
WHY hierarchical search:
|
394
|
+
- Start from top layer for global navigation
|
395
|
+
- Descend to lower layers for refinement
|
396
|
+
- Final layer 0 search for precise results
|
397
|
+
"""
|
398
|
+
if k < 1:
|
399
|
+
raise XWNodeValueError(f"k must be >= 1, got {k}")
|
400
|
+
|
401
|
+
if self._entry_point is None:
|
402
|
+
return []
|
403
|
+
|
404
|
+
if ef is None:
|
405
|
+
ef = max(self.ef_construction, k)
|
406
|
+
|
407
|
+
# Search from top layer down
|
408
|
+
ep = self._entry_point
|
409
|
+
|
410
|
+
# Navigate to layer 1
|
411
|
+
for lc in range(self._entry_layer, 0, -1):
|
412
|
+
nearest = self._search_layer(query, ep, 1, lc)
|
413
|
+
if nearest:
|
414
|
+
ep = nearest[0][1]
|
415
|
+
|
416
|
+
# Search layer 0 with ef
|
417
|
+
candidates = self._search_layer(query, ep, ef, 0)
|
418
|
+
|
419
|
+
# Return top k
|
420
|
+
candidates.sort()
|
421
|
+
return [(v, d) for d, v in candidates[:k]]
|
422
|
+
|
423
|
+
def remove_edge(self, source: str, target: str, edge_id: Optional[str] = None) -> bool:
|
424
|
+
"""Remove edge from all layers."""
|
425
|
+
removed = False
|
426
|
+
|
427
|
+
for layer in self._layers[source]:
|
428
|
+
if target in self._layers[source][layer]:
|
429
|
+
self._layers[source][layer].discard(target)
|
430
|
+
removed = True
|
431
|
+
|
432
|
+
if removed:
|
433
|
+
self._edge_count -= 1
|
434
|
+
|
435
|
+
return removed
|
436
|
+
|
437
|
+
def has_edge(self, source: str, target: str) -> bool:
|
438
|
+
"""Check if edge exists in any layer."""
|
439
|
+
for layer in self._layers.get(source, {}).values():
|
440
|
+
if target in layer:
|
441
|
+
return True
|
442
|
+
return False
|
443
|
+
|
444
|
+
def get_neighbors(self, node: str, edge_type: Optional[str] = None,
|
445
|
+
direction: str = "outgoing") -> List[str]:
|
446
|
+
"""Get neighbors from layer 0."""
|
447
|
+
return list(self._layers.get(node, {}).get(0, set()))
|
448
|
+
|
449
|
+
def neighbors(self, node: str) -> Iterator[Any]:
|
450
|
+
"""Get iterator over neighbors."""
|
451
|
+
return iter(self.get_neighbors(node))
|
452
|
+
|
453
|
+
def degree(self, node: str) -> int:
|
454
|
+
"""Get degree of node at layer 0."""
|
455
|
+
return len(self.get_neighbors(node))
|
456
|
+
|
457
|
+
def edges(self) -> Iterator[Tuple[Any, Any, Dict[str, Any]]]:
|
458
|
+
"""Iterate over all edges with properties."""
|
459
|
+
for edge_dict in self.get_edges():
|
460
|
+
yield (edge_dict['source'], edge_dict['target'], {})
|
461
|
+
|
462
|
+
def vertices(self) -> Iterator[Any]:
|
463
|
+
"""Get iterator over all vertices."""
|
464
|
+
return iter(self._vertices)
|
465
|
+
|
466
|
+
def get_edges(self, edge_type: Optional[str] = None, direction: str = "both") -> List[Dict[str, Any]]:
|
467
|
+
"""Get all edges from all layers."""
|
468
|
+
edges = []
|
469
|
+
seen = set()
|
470
|
+
|
471
|
+
for vertex, layers in self._layers.items():
|
472
|
+
for layer, neighbors in layers.items():
|
473
|
+
for neighbor in neighbors:
|
474
|
+
edge_key = (vertex, neighbor)
|
475
|
+
if edge_key not in seen:
|
476
|
+
seen.add(edge_key)
|
477
|
+
edges.append({
|
478
|
+
'source': vertex,
|
479
|
+
'target': neighbor,
|
480
|
+
'layer': layer,
|
481
|
+
'edge_type': edge_type or 'proximity'
|
482
|
+
})
|
483
|
+
|
484
|
+
return edges
|
485
|
+
|
486
|
+
def get_edge_data(self, source: str, target: str, edge_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
487
|
+
"""Get edge data."""
|
488
|
+
if self.has_edge(source, target):
|
489
|
+
return {'source': source, 'target': target, 'type': 'proximity'}
|
490
|
+
return None
|
491
|
+
|
492
|
+
# ============================================================================
|
493
|
+
# GRAPH ALGORITHMS
|
494
|
+
# ============================================================================
|
495
|
+
|
496
|
+
def shortest_path(self, source: str, target: str, edge_type: Optional[str] = None) -> List[str]:
|
497
|
+
"""Find shortest path (using layer 0)."""
|
498
|
+
if source not in self._vertices or target not in self._vertices:
|
499
|
+
return []
|
500
|
+
|
501
|
+
queue = deque([source])
|
502
|
+
visited = {source}
|
503
|
+
parent = {source: None}
|
504
|
+
|
505
|
+
while queue:
|
506
|
+
current = queue.popleft()
|
507
|
+
|
508
|
+
if current == target:
|
509
|
+
path = []
|
510
|
+
while current:
|
511
|
+
path.append(current)
|
512
|
+
current = parent[current]
|
513
|
+
return list(reversed(path))
|
514
|
+
|
515
|
+
for neighbor in self.get_neighbors(current):
|
516
|
+
if neighbor not in visited:
|
517
|
+
visited.add(neighbor)
|
518
|
+
parent[neighbor] = current
|
519
|
+
queue.append(neighbor)
|
520
|
+
|
521
|
+
return []
|
522
|
+
|
523
|
+
def find_cycles(self, start_node: str, edge_type: Optional[str] = None, max_depth: int = 10) -> List[List[str]]:
|
524
|
+
"""Find cycles (simplified)."""
|
525
|
+
return []
|
526
|
+
|
527
|
+
def traverse_graph(self, start_node: str, strategy: str = "bfs",
|
528
|
+
max_depth: int = 100, edge_type: Optional[str] = None) -> Iterator[str]:
|
529
|
+
"""Traverse graph."""
|
530
|
+
if start_node not in self._vertices:
|
531
|
+
return
|
532
|
+
|
533
|
+
visited = set()
|
534
|
+
queue = deque([start_node])
|
535
|
+
visited.add(start_node)
|
536
|
+
|
537
|
+
while queue:
|
538
|
+
current = queue.popleft()
|
539
|
+
yield current
|
540
|
+
|
541
|
+
for neighbor in self.get_neighbors(current):
|
542
|
+
if neighbor not in visited:
|
543
|
+
visited.add(neighbor)
|
544
|
+
queue.append(neighbor)
|
545
|
+
|
546
|
+
def is_connected(self, source: str, target: str, edge_type: Optional[str] = None) -> bool:
|
547
|
+
"""Check if vertices connected."""
|
548
|
+
return len(self.shortest_path(source, target)) > 0
|
549
|
+
|
550
|
+
# ============================================================================
|
551
|
+
# STANDARD OPERATIONS
|
552
|
+
# ============================================================================
|
553
|
+
|
554
|
+
def __len__(self) -> int:
|
555
|
+
"""Get number of edges across all layers."""
|
556
|
+
total = 0
|
557
|
+
for vertex_layers in self._layers.values():
|
558
|
+
for neighbors in vertex_layers.values():
|
559
|
+
total += len(neighbors)
|
560
|
+
return total // 2 # Undirected edges counted twice
|
561
|
+
|
562
|
+
def __iter__(self) -> Iterator[Dict[str, Any]]:
|
563
|
+
"""Iterate over edges."""
|
564
|
+
return iter(self.get_edges())
|
565
|
+
|
566
|
+
def to_native(self) -> Dict[str, Any]:
|
567
|
+
"""Convert to native representation."""
|
568
|
+
return {
|
569
|
+
'vertices': list(self._vertices),
|
570
|
+
'vectors': {v: list(vec) for v, vec in self._vectors.items()},
|
571
|
+
'layers': {
|
572
|
+
v: {l: list(neighbors) for l, neighbors in layers.items()}
|
573
|
+
for v, layers in self._layers.items()
|
574
|
+
},
|
575
|
+
'entry_point': self._entry_point,
|
576
|
+
'entry_layer': self._entry_layer
|
577
|
+
}
|
578
|
+
|
579
|
+
# ============================================================================
|
580
|
+
# STATISTICS
|
581
|
+
# ============================================================================
|
582
|
+
|
583
|
+
def get_statistics(self) -> Dict[str, Any]:
|
584
|
+
"""Get HNSW statistics."""
|
585
|
+
# Calculate layer distribution
|
586
|
+
layer_counts = defaultdict(int)
|
587
|
+
for vertex_layers in self._layers.values():
|
588
|
+
for layer in vertex_layers.keys():
|
589
|
+
layer_counts[layer] += 1
|
590
|
+
|
591
|
+
max_layer = max(layer_counts.keys()) if layer_counts else 0
|
592
|
+
|
593
|
+
# Average degree at each layer
|
594
|
+
avg_degrees = {}
|
595
|
+
for layer in range(max_layer + 1):
|
596
|
+
degrees = [
|
597
|
+
len(self._layers[v].get(layer, set()))
|
598
|
+
for v in self._layers
|
599
|
+
if layer in self._layers[v]
|
600
|
+
]
|
601
|
+
avg_degrees[layer] = sum(degrees) / len(degrees) if degrees else 0
|
602
|
+
|
603
|
+
return {
|
604
|
+
'vertices': len(self._vertices),
|
605
|
+
'vectors': len(self._vectors),
|
606
|
+
'edges': len(self),
|
607
|
+
'max_layer': max_layer,
|
608
|
+
'entry_layer': self._entry_layer,
|
609
|
+
'layer_distribution': dict(layer_counts),
|
610
|
+
'avg_degree_by_layer': avg_degrees,
|
611
|
+
'M': self.M,
|
612
|
+
'ef_construction': self.ef_construction,
|
613
|
+
'distance_metric': self.distance_metric
|
614
|
+
}
|
615
|
+
|
616
|
+
# ============================================================================
|
617
|
+
# UTILITY METHODS
|
618
|
+
# ============================================================================
|
619
|
+
|
620
|
+
@property
|
621
|
+
def strategy_name(self) -> str:
|
622
|
+
"""Get strategy name."""
|
623
|
+
return "HNSW"
|
624
|
+
|
625
|
+
@property
|
626
|
+
def supported_traits(self) -> List[EdgeTrait]:
|
627
|
+
"""Get supported traits."""
|
628
|
+
return [EdgeTrait.SPARSE, EdgeTrait.MULTI, EdgeTrait.DIRECTED]
|
629
|
+
|
630
|
+
def get_backend_info(self) -> Dict[str, Any]:
|
631
|
+
"""Get backend information."""
|
632
|
+
return {
|
633
|
+
'strategy': 'HNSW',
|
634
|
+
'description': 'Hierarchical Navigable Small World for ANN search',
|
635
|
+
**self.get_statistics()
|
636
|
+
}
|
637
|
+
|