xref: /plugin/elasticsearch/helper/client.php (revision 14b22d8710cdb2f9d09946b6ad563a08f2544e9a) !
1<?php
2/**
3 * DokuWiki Plugin elasticsearch (Helper Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Kieback&Peter IT <it-support@kieback-peter.de>
7 */
8
9use dokuwiki\Extension\Event;
10
11require_once dirname(__FILE__) . '/../vendor/autoload.php';
12
13/**
14 * Access to the Elastica client
15 */
16class helper_plugin_elasticsearch_client extends DokuWiki_Plugin {
17
18    /** @var array Map of ISO codes to Elasticsearch analyzer names */
19    const ANALYZERS = [
20        'ar' => 'arabic',
21        'bg' => 'bulgarian',
22        'bn' => 'bengali',
23        'ca' => 'catalan',
24        'cs' => 'czech',
25        'da' => 'danish',
26        'de' => 'german',
27        'el' => 'greek',
28        'en' => 'english',
29        'es' => 'spanish',
30        'eu' => 'basque',
31        'fa' => 'persian',
32        'fi' => 'finnish',
33        'fr' => 'french',
34        'ga' => 'irish',
35        'gl' => 'galician',
36        'hi' => 'hindi',
37        'hu' => 'hungarian',
38        'hy' => 'armenian',
39        'id' => 'indonesian',
40        'it' => 'italian',
41        'lt' => 'lithuanian',
42        'lv' => 'latvian',
43        'nl' => 'dutch',
44        'no' => 'norwegian',
45        'pt' => 'portuguese',
46        'ro' => 'romanian',
47        'ru' => 'russian',
48        'sv' => 'swedish',
49        'th' => 'thai',
50        'tr' => 'turkish',
51        ];
52    /**
53     * @var \Elastica\Client $elasticaClient
54     */
55    protected $elasticaClient = null;
56
57    /**
58     * Connects to the elastica servers and returns the client object
59     *
60     * @return \Elastica\Client
61     */
62    public function connect() {
63        if(!is_null($this->elasticaClient)) return $this->elasticaClient;
64
65        // parse servers config into DSN array
66        $dsn = ['servers' => []];
67        $servers = $this->getConf('servers');
68        $lines   = explode("\n", $servers);
69        foreach($lines as $line) {
70            list($host, $proxy) = explode(',', $line, 2);
71            list($host, $port) = explode(':', $host, 2);
72            $host = trim($host);
73            $port = (int) trim($port);
74            if(!$port) $port = 80;
75            $proxy = trim($proxy);
76            if(!$host) continue;
77            $dsn['servers'][] = compact('host', 'port', 'proxy');
78        }
79
80        $this->elasticaClient = new \Elastica\Client($dsn);
81        return $this->elasticaClient;
82    }
83
84    /**
85     * Create the index
86     *
87     * @param bool $clear rebuild index
88     * @return \Elastica\Response
89     */
90    public function createIndex($clear=false) {
91        $client = $this->connect();
92        $index = $client->getIndex($this->getConf('indexname'));
93
94        $index->create([], $clear);
95
96        $response = $this->mapAccessFields($index);
97        if ($response->hasError()) return $response;
98
99        $pluginMappings = [];
100        // plugins can supply their own mappings: ['plugin' => ['type' => 'keyword'] ]
101        Event::createAndTrigger('PLUGIN_ELASTICSEARCH_CREATEMAPPING', $pluginMappings);
102
103        if (!empty($pluginMappings)) {
104            foreach ($pluginMappings as $mapping) {
105                $response = $this->mapPluginFields($index, $mapping);
106                if ($response->hasError()) return $response;
107            }
108        }
109
110        return $response;
111    }
112
113    /**
114     * Create the field mapping: language analyzers for the content field
115     *
116     * @return \Elastica\Response
117     */
118    public function createLanguageMapping() {
119        global $conf;
120
121        $client = $this->connect();
122        $index = $client->getIndex($this->getConf('indexname'));
123        $type = $index->getType($this->getConf('documenttype'));
124
125        // default language
126        $props = [
127            'content' => [
128                'type'  => 'text',
129                'fields' => [
130                    $conf['lang'] => [
131                        'type'  => 'text',
132                        'analyzer' => $this->getLanguageAnalyzer($conf['lang'])
133                    ],
134                ]
135            ]
136        ];
137
138        // other languages as configured in the translation plugin
139        /** @var helper_plugin_translation $transplugin */
140        $transplugin = plugin_load('helper', 'translation');
141        if ($transplugin) {
142            $translations = array_diff(array_filter($transplugin->translations), [$conf['lang']]);
143            if ($translations) foreach ($translations as $lang) {
144                $props['content']['fields'][$lang] = [
145                    'type' => 'text',
146                    'analyzer' => $this->getLanguageAnalyzer($lang)
147                ];
148            }
149        }
150
151        $mapping = new \Elastica\Type\Mapping();
152        $mapping->setType($type);
153        $mapping->setProperties($props);
154        $response = $mapping->send();
155        return $response;
156    }
157
158    /**
159     * Get the correct analyzer for the given language code
160     *
161     * Returns the standard analalyzer for unknown languages
162     *
163     * @param string $lang
164     * @return string
165     */
166    protected function getLanguageAnalyzer($lang)
167    {
168        if (isset(self::ANALYZERS[$lang])) return self::ANALYZERS[$lang];
169        return 'standard';
170    }
171
172    /**
173     * Define special mappings for ACL fields
174     *
175     * Standard mapping could break the search because ACL fields
176     * might contain word-split tokens such as underscores and so must not
177     * be indexed using the standard text analyzer.
178     *
179     * @param \Elastica\Index $index
180     * @return \Elastica\Response
181     */
182    protected function mapAccessFields(\Elastica\Index $index): \Elastica\Response
183    {
184        $type = $index->getType($this->getConf('documenttype'));
185        $props = [
186            'groups_include' => [
187                'type' => 'keyword',
188            ],
189            'groups_exclude' => [
190                'type' => 'keyword',
191            ],
192            'users_include' => [
193                'type' => 'keyword',
194            ],
195            'users_exclude' => [
196                'type' => 'keyword',
197            ],
198        ];
199
200        $mapping = new \Elastica\Type\Mapping();
201        $mapping->setType($type);
202        $mapping->setProperties($props);
203        return $mapping->send();
204    }
205
206    /**
207     * Add mappings provided by plugins
208     * via PLUGIN_ELASTICSEARCH_CREATEMAPPING event
209     *
210     * @param \Elastica\Index $index
211     * @param array $props
212     * @return \Elastica\Response
213     */
214    public function mapPluginFields(\Elastica\Index $index, Array $props): \Elastica\Response
215    {
216        $type = $index->getType($this->getConf('documenttype'));
217
218        $mapping = new \Elastica\Type\Mapping();
219        $mapping->setType($type);
220        $mapping->setProperties($props);
221        return $mapping->send();
222    }
223}
224
225// vim:ts=4:sw=4:et:
226