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