colonylib 0.6.0

A library implementing the Colony metadata framework on Autonomi
Documentation
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
mod common;
use common::create_test_graph;

#[test]
fn test_enhanced_word_based_search() {
    let (mut graph, _temp_dir) = create_test_graph();

    // Create pods with different depths
    let pod1_address = "test_pod_depth_0";
    let pod2_address = "test_pod_depth_1";
    let pod3_address = "test_pod_depth_2";

    // Add pods with different depths
    graph
        .add_pod_entry(
            "Pod at Depth 0",
            pod1_address,
            "scratchpad1",
            "config1",
            "config_scratchpad1",
            0,
        )
        .unwrap();
    graph
        .add_pod_entry(
            "Pod at Depth 1",
            pod2_address,
            "scratchpad2",
            "config2",
            "config_scratchpad2",
            0,
        )
        .unwrap();
    graph
        .add_pod_entry(
            "Pod at Depth 2",
            pod3_address,
            "scratchpad3",
            "config3",
            "config_scratchpad3",
            0,
        )
        .unwrap();

    // Set different depths for the pods
    graph.update_pod_depth(pod1_address, "config1", 0).unwrap();
    graph.update_pod_depth(pod2_address, "config2", 1).unwrap();
    graph.update_pod_depth(pod3_address, "config3", 2).unwrap();

    // Add content with varying match counts
    let pod1_iri = format!("ant://{pod1_address}");
    let pod2_iri = format!("ant://{pod2_address}");
    let pod3_iri = format!("ant://{pod3_address}");

    // Pod 1 (depth 0): Contains "beatles" and "abbey" (2 matches)
    graph
        .put_quad(
            "ant://album1",
            "ant://title",
            "The Beatles Abbey Road",
            Some(&pod1_iri),
        )
        .unwrap();

    // Pod 2 (depth 1): Contains "beatles", "abbey", and "road" (3 matches)
    graph
        .put_quad(
            "ant://album2",
            "ant://description",
            "The Beatles recorded Abbey Road album",
            Some(&pod2_iri),
        )
        .unwrap();

    // Pod 3 (depth 2): Contains only "beatles" (1 match)
    graph
        .put_quad(
            "ant://album3",
            "ant://artist",
            "The Beatles",
            Some(&pod3_iri),
        )
        .unwrap();

    // Search for "beatles abbey road" - should return results ordered by match count, then by depth
    let search_results = graph
        .search_content("beatles abbey road", Some(10))
        .unwrap();
    assert!(
        !search_results.is_empty(),
        "Search results should not be empty"
    );

    // Parse the JSON to verify ordering
    let json_result: serde_json::Value = serde_json::from_str(&search_results).unwrap();
    let bindings = json_result["results"]["bindings"].as_array().unwrap();
    assert!(bindings.len() >= 3, "Should have at least 3 search results");

    // Verify that results are ordered by match count (descending) then by depth (ascending)
    // Both "The Beatles Abbey Road" and "The Beatles recorded Abbey Road album" have 3 matches
    // But "The Beatles recorded Abbey Road album" should come first if it has lower depth
    // "The Beatles" has only 1 match so should come last

    // Check that results with more matches come before results with fewer matches
    let first_match_count: i32 = bindings[0]
        .get("match_count")
        .and_then(|v| v.get("value"))
        .and_then(|v| v.as_str())
        .and_then(|s| s.parse().ok())
        .unwrap_or(0);
    let last_match_count: i32 = bindings[bindings.len() - 1]
        .get("match_count")
        .and_then(|v| v.get("value"))
        .and_then(|v| v.as_str())
        .and_then(|s| s.parse().ok())
        .unwrap_or(0);

    assert!(
        first_match_count >= last_match_count,
        "Results should be ordered by match count (descending): first={first_match_count}, last={last_match_count}"
    );

    // Test single word search
    let single_word_results = graph.search_content("beatles", Some(10)).unwrap();
    let single_json: serde_json::Value = serde_json::from_str(&single_word_results).unwrap();
    let single_bindings = single_json["results"]["bindings"].as_array().unwrap();
    assert!(
        single_bindings.len() >= 3,
        "Single word search should find all Beatles references"
    );

    // Test empty search
    let empty_results = graph.search_content("", Some(10)).unwrap();
    assert_eq!(
        empty_results, "[]",
        "Empty search should return empty array"
    );

    // Test search with no matches
    let no_match_results = graph.search_content("nonexistent", Some(10)).unwrap();
    let no_match_json: serde_json::Value = serde_json::from_str(&no_match_results).unwrap();
    let no_match_bindings = no_match_json["results"]["bindings"].as_array().unwrap();
    assert_eq!(
        no_match_bindings.len(),
        0,
        "Search with no matches should return empty results"
    );
}

#[test]
fn test_word_splitting_and_or_logic() {
    let (mut graph, _temp_dir) = create_test_graph();

    let pod_address = "test_pod_or_logic";
    graph
        .add_pod_entry(
            "Test Pod",
            pod_address,
            "scratchpad",
            "config",
            "config_scratchpad",
            0,
        )
        .unwrap();

    let pod_iri = format!("ant://{pod_address}");

    // Add content that matches different combinations of words
    graph
        .put_quad(
            "ant://doc1",
            "ant://title",
            "The Beatles are great",
            Some(&pod_iri),
        )
        .unwrap();
    graph
        .put_quad(
            "ant://doc2",
            "ant://title",
            "Abbey Road is an album",
            Some(&pod_iri),
        )
        .unwrap();
    graph
        .put_quad(
            "ant://doc3",
            "ant://title",
            "Road trip to nowhere",
            Some(&pod_iri),
        )
        .unwrap();

    // Search for "beatles abbey road" should find all three documents
    // doc1 matches "beatles", doc2 matches "abbey" and "road", doc3 matches "road"
    let search_results = graph
        .search_content("beatles abbey road", Some(10))
        .unwrap();
    let json_result: serde_json::Value = serde_json::from_str(&search_results).unwrap();
    let bindings = json_result["results"]["bindings"].as_array().unwrap();

    assert_eq!(
        bindings.len(),
        3,
        "Should find all three documents with OR logic"
    );

    // Verify that doc2 comes first (2 matches), then doc1 and doc3 (1 match each)
    let first_result = bindings[0]["object"]["value"].as_str().unwrap_or("");
    assert!(
        first_result.contains("Abbey Road"),
        "First result should have most matches"
    );
}

#[test]
fn test_quoted_phrase_search() {
    let (mut graph, _temp_dir) = create_test_graph();

    let pod_address = "test_pod_quotes";
    graph
        .add_pod_entry(
            "Test Pod",
            pod_address,
            "scratchpad",
            "config",
            "config_scratchpad",
            0,
        )
        .unwrap();

    let pod_iri = format!("ant://{pod_address}");

    // Add content with exact phrases and individual words
    graph
        .put_quad(
            "ant://doc1",
            "ant://title",
            "The Beatles Abbey Road album",
            Some(&pod_iri),
        )
        .unwrap();
    graph
        .put_quad(
            "ant://doc2",
            "ant://title",
            "Abbey Road is great",
            Some(&pod_iri),
        )
        .unwrap();
    graph
        .put_quad(
            "ant://doc3",
            "ant://title",
            "The Beatles recorded many albums",
            Some(&pod_iri),
        )
        .unwrap();
    graph
        .put_quad(
            "ant://doc4",
            "ant://title",
            "Road trip with the band",
            Some(&pod_iri),
        )
        .unwrap();

    // Search for 'the beatles "abbey road"' should find:
    // - doc1: matches "the", "beatles", AND "abbey road" phrase (3 matches)
    // - doc2: matches "abbey road" phrase (1 match)
    // - doc3: matches "the" and "beatles" (2 matches)
    // - doc4: matches "the" (1 match)
    let search_results = graph
        .search_content(r#"the beatles "abbey road""#, Some(10))
        .unwrap();
    let json_result: serde_json::Value = serde_json::from_str(&search_results).unwrap();
    let bindings = json_result["results"]["bindings"].as_array().unwrap();

    // Should find all 4 documents since they all contain at least one search term
    assert_eq!(bindings.len(), 4, "Should find exactly 4 documents");

    // Check that doc1 comes first (should have 3 matches)
    let first_result = bindings[0]["object"]["value"].as_str().unwrap_or("");
    assert!(
        first_result.contains("The Beatles Abbey Road"),
        "First result should be the one with most matches"
    );

    // Verify that all results contain at least one of the search terms
    for binding in bindings {
        let text = binding["object"]["value"]
            .as_str()
            .unwrap_or("")
            .to_lowercase();
        let has_the = text.contains("the");
        let has_beatles = text.contains("beatles");
        let has_abbey_road_phrase = text.contains("abbey road");

        assert!(
            has_the || has_beatles || has_abbey_road_phrase,
            "Result '{text}' should contain at least one search term"
        );
    }
}

#[test]
fn test_multiple_quoted_phrases() {
    let (mut graph, _temp_dir) = create_test_graph();

    let pod_address = "test_pod_multi_quotes";
    graph
        .add_pod_entry(
            "Test Pod",
            pod_address,
            "scratchpad",
            "config",
            "config_scratchpad",
            0,
        )
        .unwrap();

    let pod_iri = format!("ant://{pod_address}");

    // Add content to test multiple quoted phrases
    graph
        .put_quad(
            "ant://doc1",
            "ant://title",
            "The Beatles Abbey Road is a great album",
            Some(&pod_iri),
        )
        .unwrap();
    graph
        .put_quad(
            "ant://doc2",
            "ant://title",
            "Led Zeppelin IV is also great",
            Some(&pod_iri),
        )
        .unwrap();
    graph
        .put_quad(
            "ant://doc3",
            "ant://title",
            "Abbey Road by The Beatles",
            Some(&pod_iri),
        )
        .unwrap();

    // Search for '"the beatles" "abbey road"' should find:
    // - doc1: matches both phrases (2 matches)
    // - doc3: matches both phrases (2 matches)
    // - doc2: matches neither phrase (0 matches) - should not appear
    let search_results = graph
        .search_content(r#""the beatles" "abbey road""#, Some(10))
        .unwrap();
    let json_result: serde_json::Value = serde_json::from_str(&search_results).unwrap();
    let bindings = json_result["results"]["bindings"].as_array().unwrap();

    // Should find 2 documents (doc1, doc3) but not doc2
    assert_eq!(
        bindings.len(),
        2,
        "Should find exactly 2 documents with both phrases"
    );

    // Verify that all results contain both phrases
    for binding in bindings {
        let text = binding["object"]["value"]
            .as_str()
            .unwrap_or("")
            .to_lowercase();
        assert!(
            text.contains("the beatles") && text.contains("abbey road"),
            "Result should contain both 'the beatles' and 'abbey road' phrases"
        );
    }
}

#[test]
fn test_search_term_parsing() {
    // Test the parsing function directly by creating a simple test
    let (graph, _temp_dir) = create_test_graph();

    // Test various parsing scenarios by checking the actual search behavior
    let test_cases = vec![
        ("simple words", vec!["simple", "words"]),
        ("\"quoted phrase\"", vec!["quoted phrase"]),
        (
            "word \"quoted phrase\" word",
            vec!["word", "quoted phrase", "word"],
        ),
        (
            "\"first phrase\" \"second phrase\"",
            vec!["first phrase", "second phrase"],
        ),
        (
            "before \"middle phrase\" after",
            vec!["before", "middle phrase", "after"],
        ),
        ("\"unclosed quote", vec!["unclosed quote"]), // Should handle unclosed quotes gracefully
        ("", vec![]),                                 // Empty string
        ("   spaced   words   ", vec!["spaced", "words"]), // Extra whitespace
    ];

    for (input, expected) in test_cases {
        // We can't directly test the private parse_search_terms function,
        // but we can verify the behavior by checking if empty searches return empty results
        if expected.is_empty() {
            let result = graph.search_content(input, Some(1)).unwrap();
            assert_eq!(
                result, "[]",
                "Empty search '{input}' should return empty results"
            );
        } else {
            // For non-empty searches, just verify they don't crash and return valid JSON
            let result = graph.search_content(input, Some(1)).unwrap();
            let _: serde_json::Value = serde_json::from_str(&result)
                .unwrap_or_else(|_| panic!("Search '{input}' should return valid JSON"));
        }
    }
}