Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
search.go17.6 kB
// Package search provides APOC search functions. // // This package implements all apoc.search.* functions for full-text // search and pattern matching in graph data. package search import ( "regexp" "strings" "github.com/orneryd/nornicdb/apoc/storage" ) // NodeType represents a graph node. type NodeType = storage.Node // Relationship represents a graph relationship. type Relationship = storage.Relationship // Storage is the interface for database operations. var Storage storage.Storage = storage.NewInMemoryStorage() // Node searches nodes by property value. // // Example: // // apoc.search.node('Person', 'name', 'Alice') => matching nodes func Node(label, property string, value interface{}) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if propVal, ok := node.Properties[property]; ok { if propVal == value { results = append(results, node) } } } return results } // NodeAll searches nodes matching all criteria. // // Example: // // apoc.search.nodeAll('Person', {name: 'Alice', age: 30}) => matching nodes func NodeAll(label string, criteria map[string]interface{}) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } matches := true for key, value := range criteria { if propVal, ok := node.Properties[key]; !ok || propVal != value { matches = false break } } if matches { results = append(results, node) } } return results } // NodeAny searches nodes matching any criteria. // // Example: // // apoc.search.nodeAny('Person', {name: 'Alice', name: 'Bob'}) => matching nodes func NodeAny(label string, criteria map[string]interface{}) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } for key, value := range criteria { if propVal, ok := node.Properties[key]; ok && propVal == value { results = append(results, node) break } } } return results } // NodeReduced searches with reduced results. // // Example: // // apoc.search.nodeReduced('Person', {name: 'A*'}, 10) => top 10 matches func NodeReduced(label string, criteria map[string]interface{}, limit int) []*NodeType { nodes := NodeAll(label, criteria) if len(nodes) > limit { return nodes[:limit] } return nodes } // MultiSearchAll searches multiple labels. // // Example: // // apoc.search.multiSearchAll(['Person', 'Company'], {name: 'Alice'}) => matches func MultiSearchAll(labels []string, criteria map[string]interface{}) []*NodeType { results := make([]*NodeType, 0) for _, label := range labels { nodes := NodeAll(label, criteria) results = append(results, nodes...) } return results } // MultiSearchAny searches multiple labels with any match. // // Example: // // apoc.search.multiSearchAny(['Person', 'Company'], {name: 'Alice'}) => matches func MultiSearchAny(labels []string, criteria map[string]interface{}) []*NodeType { return MultiSearchAll(labels, criteria) } // Parallel searches in parallel across labels. // // Example: // // apoc.search.parallel(['Person', 'Company'], 'name', 'Alice') => matches func Parallel(labels []string, property string, value interface{}) []*NodeType { // Placeholder - would execute parallel searches results := make([]*NodeType, 0) for _, label := range labels { nodes := Node(label, property, value) results = append(results, nodes...) } return results } // FullText performs full-text search. // // Example: // // apoc.search.fullText('Person', 'name', 'Alice Bob') => matching nodes func FullText(label, property, query string) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } queryLower := strings.ToLower(query) words := strings.Fields(queryLower) results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if propVal, ok := node.Properties[property]; ok { if strVal, ok := propVal.(string); ok { strLower := strings.ToLower(strVal) for _, word := range words { if strings.Contains(strLower, word) { results = append(results, node) break } } } } } return results } // Fuzzy performs fuzzy search. // // Example: // // apoc.search.fuzzy('Person', 'name', 'Alise', 2) => matches within edit distance 2 func Fuzzy(label, property, value string, maxDistance int) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } valueLower := strings.ToLower(value) results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if propVal, ok := node.Properties[property]; ok { if strVal, ok := propVal.(string); ok { distance := levenshteinDistance(strings.ToLower(strVal), valueLower) if distance <= maxDistance { results = append(results, node) } } } } return results } // Regex searches using regular expressions. // // Example: // // apoc.search.regex('Person', 'email', '.*@example\\.com') => matching nodes func Regex(label, property, pattern string) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } re, err := regexp.Compile(pattern) if err != nil { return []*NodeType{} } results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if propVal, ok := node.Properties[property]; ok { if strVal, ok := propVal.(string); ok { if re.MatchString(strVal) { results = append(results, node) } } } } return results } // Prefix searches by prefix. // // Example: // // apoc.search.prefix('Person', 'name', 'Ali') => names starting with 'Ali' func Prefix(label, property, prefix string) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } prefixLower := strings.ToLower(prefix) results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if propVal, ok := node.Properties[property]; ok { if strVal, ok := propVal.(string); ok { if strings.HasPrefix(strings.ToLower(strVal), prefixLower) { results = append(results, node) } } } } return results } // Suffix searches by suffix. // // Example: // // apoc.search.suffix('Person', 'email', '@example.com') => emails ending with suffix func Suffix(label, property, suffix string) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } suffixLower := strings.ToLower(suffix) results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if propVal, ok := node.Properties[property]; ok { if strVal, ok := propVal.(string); ok { if strings.HasSuffix(strings.ToLower(strVal), suffixLower) { results = append(results, node) } } } } return results } // Contains searches for substring. // // Example: // // apoc.search.contains('Person', 'name', 'lic') => names containing 'lic' func Contains(label, property, substring string) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } substringLower := strings.ToLower(substring) results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if propVal, ok := node.Properties[property]; ok { if strVal, ok := propVal.(string); ok { if strings.Contains(strings.ToLower(strVal), substringLower) { results = append(results, node) } } } } return results } // Range searches within a range. // // Example: // // apoc.search.range('Person', 'age', 18, 65) => nodes with age 18-65 func Range(label, property string, minVal, maxVal interface{}) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if propVal, ok := node.Properties[property]; ok { if inRange(propVal, minVal, maxVal) { results = append(results, node) } } } return results } // inRange checks if value is within range func inRange(value, minVal, maxVal interface{}) bool { switch v := value.(type) { case int: min, minOk := toInt(minVal) max, maxOk := toInt(maxVal) return minOk && maxOk && v >= min && v <= max case int64: min, minOk := toInt64(minVal) max, maxOk := toInt64(maxVal) return minOk && maxOk && v >= min && v <= max case float64: min, minOk := toFloat64(minVal) max, maxOk := toFloat64(maxVal) return minOk && maxOk && v >= min && v <= max case string: minStr, minOk := minVal.(string) maxStr, maxOk := maxVal.(string) return minOk && maxOk && v >= minStr && v <= maxStr } return false } func toInt(v interface{}) (int, bool) { switch val := v.(type) { case int: return val, true case int64: return int(val), true case float64: return int(val), true } return 0, false } func toInt64(v interface{}) (int64, bool) { switch val := v.(type) { case int: return int64(val), true case int64: return val, true case float64: return int64(val), true } return 0, false } func toFloat64(v interface{}) (float64, bool) { switch val := v.(type) { case int: return float64(val), true case int64: return float64(val), true case float64: return val, true } return 0, false } // In searches for values in a list. // // Example: // // apoc.search.in('Person', 'status', ['active', 'pending']) => matching nodes func In(label, property string, values []interface{}) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } valueSet := make(map[interface{}]bool) for _, v := range values { valueSet[v] = true } results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if propVal, ok := node.Properties[property]; ok { if valueSet[propVal] { results = append(results, node) } } } return results } // NotIn searches for values not in a list. // // Example: // // apoc.search.notIn('Person', 'status', ['deleted', 'banned']) => matching nodes func NotIn(label, property string, values []interface{}) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } valueSet := make(map[interface{}]bool) for _, v := range values { valueSet[v] = true } results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if propVal, ok := node.Properties[property]; ok { if !valueSet[propVal] { results = append(results, node) } } } return results } // Exists searches for nodes with property. // // Example: // // apoc.search.exists('Person', 'email') => nodes with email property func Exists(label, property string) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if _, ok := node.Properties[property]; ok { results = append(results, node) } } return results } // Missing searches for nodes without property. // // Example: // // apoc.search.missing('Person', 'email') => nodes without email func Missing(label, property string) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if _, ok := node.Properties[property]; !ok { results = append(results, node) } } return results } // Null searches for nodes with null property. // // Example: // // apoc.search.null('Person', 'middleName') => nodes with null middleName func Null(label, property string) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if propVal, ok := node.Properties[property]; ok && propVal == nil { results = append(results, node) } } return results } // NotNull searches for nodes with non-null property. // // Example: // // apoc.search.notNull('Person', 'email') => nodes with non-null email func NotNull(label, property string) []*NodeType { nodes, err := Storage.AllNodes() if err != nil { return []*NodeType{} } results := make([]*NodeType, 0) for _, node := range nodes { if !hasLabel(node, label) { continue } if propVal, ok := node.Properties[property]; ok && propVal != nil { results = append(results, node) } } return results } // hasLabel checks if node has a specific label func hasLabel(node *NodeType, label string) bool { for _, l := range node.Labels { if l == label { return true } } return false } // Match matches pattern against property. // // Example: // // apoc.search.match('Person', 'name', 'A*') => names matching pattern func Match(label, property, pattern string) []*NodeType { // Convert wildcard pattern to regex regexPattern := strings.ReplaceAll(pattern, "*", ".*") regexPattern = strings.ReplaceAll(regexPattern, "?", ".") regexPattern = "^" + regexPattern + "$" return Regex(label, property, regexPattern) } // Score calculates relevance scores. // // Example: // // apoc.search.score(nodes, 'name', 'Alice') => nodes with scores func Score(nodes []*NodeType, property, query string) []map[string]interface{} { results := make([]map[string]interface{}, 0) queryLower := strings.ToLower(query) for _, node := range nodes { if val, ok := node.Properties[property]; ok { if strVal, ok := val.(string); ok { score := calculateScore(strings.ToLower(strVal), queryLower) results = append(results, map[string]interface{}{ "node": node, "score": score, }) } } } return results } // calculateScore calculates simple relevance score. func calculateScore(text, query string) float64 { if text == query { return 1.0 } if strings.Contains(text, query) { return 0.8 } if strings.HasPrefix(text, query) { return 0.6 } // Levenshtein distance-based score distance := levenshteinDistance(text, query) maxLen := len(text) if len(query) > maxLen { maxLen = len(query) } if maxLen == 0 { return 0 } return 1.0 - (float64(distance) / float64(maxLen)) } // levenshteinDistance calculates edit distance. func levenshteinDistance(s1, s2 string) int { if len(s1) == 0 { return len(s2) } if len(s2) == 0 { return len(s1) } matrix := make([][]int, len(s1)+1) for i := range matrix { matrix[i] = make([]int, len(s2)+1) matrix[i][0] = i } for j := 0; j <= len(s2); j++ { matrix[0][j] = j } for i := 1; i <= len(s1); i++ { for j := 1; j <= len(s2); j++ { cost := 0 if s1[i-1] != s2[j-1] { cost = 1 } matrix[i][j] = min( matrix[i-1][j]+1, min(matrix[i][j-1]+1, matrix[i-1][j-1]+cost), ) } } return matrix[len(s1)][len(s2)] } func min(a, b int) int { if a < b { return a } return b } // Highlight highlights matching terms. // // Example: // // apoc.search.highlight('Hello Alice', 'Alice', '<b>', '</b>') => 'Hello <b>Alice</b>' func Highlight(text, query, prefix, suffix string) string { re := regexp.MustCompile("(?i)" + regexp.QuoteMeta(query)) return re.ReplaceAllString(text, prefix+"$0"+suffix) } // Suggest provides search suggestions. // // Example: // // apoc.search.suggest('Person', 'name', 'Ali', 5) => ['Alice', 'Alison', ...] func Suggest(label, property, prefix string, limit int) []string { nodes := Prefix(label, property, prefix) suggestions := make([]string, 0, limit) for i, node := range nodes { if i >= limit { break } if propVal, ok := node.Properties[property]; ok { if strVal, ok := propVal.(string); ok { suggestions = append(suggestions, strVal) } } } return suggestions } // Autocomplete provides autocomplete suggestions. // // Example: // // apoc.search.autocomplete('Person', 'name', 'Al') => suggestions func Autocomplete(label, property, prefix string) []string { return Suggest(label, property, prefix, 10) } // DidYouMean provides spelling suggestions. // // Example: // // apoc.search.didYouMean('Person', 'name', 'Alise') => ['Alice'] func DidYouMean(label, property, query string) []string { // Get fuzzy matches within distance 2 nodes := Fuzzy(label, property, query, 2) suggestions := make([]string, 0) for _, node := range nodes { if propVal, ok := node.Properties[property]; ok { if strVal, ok := propVal.(string); ok { suggestions = append(suggestions, strVal) } } } return suggestions } // Index creates a search index. // // Example: // // apoc.search.index.create('Person', ['name', 'email']) => index created func Index(label string, properties []string) error { // Placeholder - would create search index return nil } // DropIndex drops a search index. // // Example: // // apoc.search.index.drop('Person', ['name']) => index dropped func DropIndex(label string, properties []string) error { // Placeholder - would drop search index return nil } // Reindex rebuilds search indexes. // // Example: // // apoc.search.reindex('Person') => reindexed func Reindex(label string) error { // Placeholder - would rebuild indexes return nil }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server