Passed
Push — master ( ab6f49...c762ff )
by
unknown
16:59 queued 08:07
created

XapianIndexService::openWritableDatabase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 5
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Search\Xapian;
8
9
/**
10
 * Service responsible for indexing documents into the Xapian database.
11
 */
12
final class XapianIndexService
13
{
14
    private const DEFAULT_LANGUAGE = 'english';
15
16
    public function __construct(
17
        private readonly SearchIndexPathResolver $indexPathResolver,
18
    ) {
19
    }
20
21
    /**
22
     * Indexes a simple demo document so we can verify that search works end-to-end.
23
     *
24
     * @return int The Xapian internal document id
25
     *
26
     * @throws \RuntimeException When indexing fails
27
     */
28
    public function indexDemoDocument(): int
29
    {
30
        $now = new \DateTimeImmutable('now');
31
32
        $fields = [
33
            'title'      => 'Demo test document',
34
            'content'    => 'This is a test document indexed from XapianIndexService in Chamilo 2.',
35
            'created_at' => $now->format(\DATE_ATOM),
36
        ];
37
38
        $terms = [
39
            'XTdemo',
40
            'XTchamilo',
41
        ];
42
43
        return $this->indexDocument($fields, $terms);
44
    }
45
46
    /**
47
     * Indexes a generic document.
48
     *
49
     * @param array<string,mixed> $fields Arbitrary data to store and index as free-text
50
     * @param string[]            $terms  Optional list of additional terms to add to the document
51
     * @param string|null         $language Language used for stemming (defaults to english)
52
     *
53
     * @return int The Xapian internal document id
54
     *
55
     * @throws \RuntimeException When Xapian fails during indexing
56
     */
57
    public function indexDocument(
58
        array $fields,
59
        array $terms = [],
60
        ?string $language = null,
61
    ): int {
62
        if (!\class_exists(\XapianWritableDatabase::class)) {
63
            throw new \RuntimeException('Xapian PHP extension is not loaded.');
64
        }
65
66
        $db = $this->openWritableDatabase();
67
68
        $doc     = new \XapianDocument();
69
        $termGen = new \XapianTermGenerator();
70
71
        $lang    = $language ?: self::DEFAULT_LANGUAGE;
72
        $stemmer = new \XapianStem($lang);
73
74
        $termGen->set_stemmer($stemmer);
75
        $termGen->set_document($doc);
76
77
        // Index all field values as free-text (title, content, etc.)
78
        foreach ($fields as $value) {
79
            if ($value === null) {
80
                continue;
81
            }
82
83
            if (!\is_string($value)) {
84
                $value = (string) $value;
85
            }
86
87
            $termGen->index_text($value, 1);
88
        }
89
90
        // Add explicit terms if provided
91
        foreach ($terms as $term) {
92
            $term = (string) $term;
93
            if ($term === '') {
94
                continue;
95
            }
96
97
            $doc->add_term($term, 1);
98
        }
99
100
        // Store fields as serialized payload (compatible with the search service decode)
101
        $doc->set_data(\serialize($fields));
102
103
        try {
104
            $docId = $db->add_document($doc);
105
            $db->flush();
106
107
            error_log('[Xapian] XapianIndexService::indexDocument: document added with docId='
108
                .var_export($docId, true)
109
            );
110
111
            return $docId;
112
        } catch (\Throwable $e) {
113
            throw new \RuntimeException(
114
                \sprintf('Failed to index document in Xapian: %s', $e->getMessage()),
115
                0,
116
                $e
117
            );
118
        }
119
    }
120
121
    /**
122
     * Deletes a document from the Xapian index using its internal document id.
123
     *
124
     * @throws \RuntimeException When Xapian fails during deletion
125
     */
126
    public function deleteDocument(int $docId): void
127
    {
128
        if (!\class_exists(\XapianWritableDatabase::class)) {
129
            throw new \RuntimeException('Xapian PHP extension is not loaded.');
130
        }
131
132
        $db = $this->openWritableDatabase();
133
134
        try {
135
            error_log('[Xapian] XapianIndexService::deleteDocument: deleting docId='
136
                .var_export($docId, true)
137
            );
138
139
            $db->delete_document($docId);
140
            $db->flush();
141
        } catch (\Throwable $e) {
142
            throw new \RuntimeException(
143
                \sprintf('Failed to delete document in Xapian: %s', $e->getMessage()),
144
                0,
145
                $e
146
            );
147
        }
148
    }
149
150
    /**
151
     * Opens the writable Xapian database using DB_CREATE_OR_OPEN.
152
     */
153
    private function openWritableDatabase(): \XapianWritableDatabase
154
    {
155
        $indexDir = $this->indexPathResolver->getIndexDir();
156
157
        return new \XapianWritableDatabase($indexDir, \Xapian::DB_CREATE_OR_OPEN);
158
    }
159
}
160