File size: 9,002 Bytes
7545355
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import logging
import re
import pickle
from pathlib import Path
from typing import List, Dict, Any, Tuple
from collections import defaultdict
from tqdm import tqdm

logger = logging.getLogger(__name__)

class ASTIndex:
    """
    Abstract Syntax Tree index for structural code/SQL pattern matching.
    Extracts key structural features (keywords, table names, function calls, etc.)
    """
    
    def __init__(self, language: str = "sql"):
        self.language = language
        self.inverted_index = defaultdict(set)  # feature -> set of doc_ids
        self.doc_store = {}  # doc_id -> metadata
        self.doc_counter = 0
        
        logger.info(f"Initializing AST index for language: {language}")

    def _extract_features(self, code: str) -> List[str]:
        """Extract structural features from code/SQL/MQL or natural language."""
        features = []
        if self.language == "mongodb":
            features.extend(self._extract_mongodb_features(code))
            # If no MQL features found, try NL feature extraction
            if not features:
                features.extend(self._extract_nl_mongo_features(code))
        elif self.language == "sql":
            features.extend(self._extract_sql_features(code))
        else:
            features.extend(self._extract_python_features(code))
        return features

    def _extract_mongodb_features(self, mql: str) -> List[str]:
        """Extract MongoDB/MQL-specific structural features."""
        features = []
        mql_lower = mql.lower()

        # Collection names: db.<collection>.
        collection_pattern = r'db\.([a-zA-Z_][a-zA-Z0-9_]*)\.'
        collections = re.findall(collection_pattern, mql_lower)
        for col in collections:
            features.append(f"collection:{col}")

        # MQL operations
        operations = [
            "find", "aggregate", "insertone", "insertmany",
            "updateone", "updatemany", "deleteone", "deletemany",
            "countdocuments", "estimatedocumentcount", "distinct",
            "sort", "limit", "skip", "group", "match", "project",
            "lookup", "unwind", "count"
        ]
        for op in operations:
            if op in mql_lower:
                features.append(f"op:{op}")

        # Aggregation pipeline stages
        stages = ["$match", "$group", "$project", "$sort", "$limit", "$skip",
                  "$lookup", "$unwind", "$count", "$addfields", "$set"]
        for stage in stages:
            if stage.lower() in mql_lower:
                features.append(f"stage:{stage}")

        # Common operators
        operators = ["$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in",
                     "$and", "$or", "$not", "$regex", "$exists"]
        for op in operators:
            if op.lower() in mql_lower:
                features.append(f"filter:{op}")

        return features

    def _extract_nl_mongo_features(self, text: str) -> List[str]:
        """Extract features from natural language questions that map to MongoDB patterns."""
        features = []
        text_lower = text.lower()

        # Intent keywords → MQL operations
        intent_map = {
            "how many": "op:count",
            "count": "op:count",
            "number of": "op:count",
            "total number of": "op:count",
            "find": "op:find",
            "get": "op:find",
            "show": "op:find",
            "list": "op:find",
            "select": "op:find",
            "average": "op:aggregate",
            "avg": "op:aggregate",
            "mean": "op:aggregate",
            "sum": "op:aggregate",
            "total": "op:aggregate",
            "maximum": "op:aggregate",
            "max": "op:aggregate",
            "minimum": "op:aggregate",
            "min": "op:aggregate",
            "group by": "op:aggregate",
            "sort": "op:sort",
            "order by": "op:sort",
            "top": "op:sort",
            "limit": "op:limit",
            "first": "op:limit",
            "join": "op:lookup",
            "distinct": "op:distinct",
            "unique": "op:distinct",
        }
        for phrase, feature in intent_map.items():
            if phrase in text_lower:
                features.append(feature)

        # Try to extract collection/entity names (simple heuristic)
        # Remove common question words and extract remaining nouns
        stopwords = {"how", "many", "what", "which", "who", "is", "are", "the",
                     "a", "an", "of", "in", "on", "for", "do", "we", "have",
                     "does", "that", "with", "and", "or", "to", "from", "by"}
        words = re.findall(r'[a-zA-Z_]+', text_lower)
        for word in words:
            if word not in stopwords and len(word) > 2:
                features.append(f"entity:{word}")

        return features
    
    def add(
        self, 
        code_snippets: List[str], 
        metadata_list: List[Dict[str, Any]],
        show_progress: bool = True
    ):
        """
        Add code snippets and their metadata to the AST index.
        
        Args:
            code_snippets: List of code/SQL strings
            metadata_list: List of metadata dicts for each snippet
            show_progress: Whether to show progress bar
        """
        if len(code_snippets) != len(metadata_list):
            raise ValueError("Number of code snippets must match number of metadata entries")
        
        iterator = zip(code_snippets, metadata_list)
        if show_progress:
            iterator = tqdm(
                iterator, 
                total=len(code_snippets),
                desc="Building AST index",
                unit="snippet"
            )
        
        for code, metadata in iterator:
            doc_id = self.doc_counter
            self.doc_counter += 1
            
            # Extract features
            features = self._extract_features(code)
            
            # Add to inverted index
            for feature in features:
                self.inverted_index[feature].add(doc_id)
            
            # Store metadata
            self.doc_store[doc_id] = metadata
        
        logger.info(f"✅ AST index built with {len(self.doc_store)} documents and {len(self.inverted_index)} unique features")
    
    def search(
        self, 
        query_code: str, 
        k: int = 5
    ) -> List[Tuple[Dict[str, Any], float]]:
        """
        Search for documents matching the query's structural features.
        
        Args:
            query_code: Query code/SQL string
            k: Number of results to return
            
        Returns:
            List of (metadata, score) tuples, sorted by score descending
        """
        if not self.doc_store:
            logger.warning("AST index is empty, returning empty results")
            return []
        
        # Extract query features
        query_features = self._extract_features(query_code)
        
        if not query_features:
            logger.warning("No features extracted from query")
            return []
        
        # Score documents based on feature overlap
        doc_scores = defaultdict(float)
        
        for feature in query_features:
            if feature in self.inverted_index:
                matching_docs = self.inverted_index[feature]
                # Simple scoring: each matching feature adds 1 point
                for doc_id in matching_docs:
                    doc_scores[doc_id] += 1.0
        
        # Sort by score and return top-k
        sorted_docs = sorted(doc_scores.items(), key=lambda x: x[1], reverse=True)[:k]
        
        results = []
        for doc_id, score in sorted_docs:
            if doc_id in self.doc_store:
                results.append((self.doc_store[doc_id], score))
        
        return results
    
    def save(self, path: str):
        """Save AST index to disk."""
        path = Path(path)
        path.parent.mkdir(parents=True, exist_ok=True)
        
        with open(path, "wb") as f:
            pickle.dump({
                "language": self.language,
                "inverted_index": dict(self.inverted_index),
                "doc_store": self.doc_store,
                "doc_counter": self.doc_counter
            }, f)
        
        logger.info(f"✅ AST index saved to {path}")
    
    @classmethod
    def load(cls, path: str) -> "ASTIndex":
        """Load AST index from disk."""
        path = Path(path)
        
        if not path.exists():
            raise FileNotFoundError(f"AST index not found at {path}")
        
        with open(path, "rb") as f:
            data = pickle.load(f)
        
        instance = cls(language=data["language"])
        instance.inverted_index = defaultdict(set, data["inverted_index"])
        instance.doc_store = data["doc_store"]
        instance.doc_counter = data["doc_counter"]
        
        logger.info(f"✅ Loaded AST index with {len(instance.doc_store)} documents from {path}")
        return instance