Browse Source

First commit

Chimo 1 year ago
commit
8c942bed36
6 changed files with 572 additions and 0 deletions
  1. 1
    0
      .gitignore
  2. 93
    0
      ElasticSearchPlugin.php
  3. 49
    0
      README.md
  4. 6
    0
      composer.json
  5. 267
    0
      composer.lock
  6. 156
    0
      lib/ElasticSearch.php

+ 1
- 0
.gitignore View File

@@ -0,0 +1 @@
1
+vendor

+ 93
- 0
ElasticSearchPlugin.php View File

@@ -0,0 +1,93 @@
1
+<?php
2
+
3
+if (!defined('GNUSOCIAL')) {
4
+    exit(1);
5
+}
6
+
7
+require('lib/ElasticSearch.php');
8
+
9
+class ElasticSearchPlugin extends Plugin
10
+{
11
+    const VERSION = '0.0.1';
12
+
13
+    function onGetSearchEngine(Memcached_DataObject $target, $table, &$search_engine)
14
+    {
15
+        if ($this->isEnabled()) {
16
+            $engine = $this->createEngine($target);
17
+
18
+            // TODO: Error handling
19
+
20
+            $search_engine = $engine;
21
+
22
+            return false;
23
+        }
24
+
25
+        return true;
26
+    }
27
+
28
+    function onEndNoticeSaveWeb($action, $notice)
29
+    {
30
+        $this->indexNotice($notice);
31
+
32
+        return true;
33
+    }
34
+
35
+    function onEndNoticeSave($notice)
36
+    {
37
+        $this->indexNotice($notice);
38
+
39
+        return true;
40
+    }
41
+
42
+    function indexNotice($notice)
43
+    {
44
+        if ($this->isEnabled()) {
45
+            $engine = $this->createEngine(new Notice());
46
+
47
+            $response = $engine->index($notice);
48
+
49
+            // TODO: Error handling
50
+        }
51
+    }
52
+
53
+    function createEngine($target)
54
+    {
55
+        $index_name = $this->getIndexname();
56
+        $hosts = common_config('elasticsearch', 'hosts');
57
+
58
+        if ($hosts === false) {
59
+            $hosts = [ '127.0.0.1:9200' ];
60
+        }
61
+
62
+        return new ElasticSearch($target, null, $index_name, $hosts);
63
+    }
64
+
65
+    function getIndexName()
66
+    {
67
+        $index_name = common_config('elasticsearch', 'index_name');
68
+
69
+        if ($index_name === false) {
70
+            $index_name = 'gnusocial';
71
+        }
72
+
73
+        return $index_name;
74
+    }
75
+
76
+    function isEnabled()
77
+    {
78
+        return common_config('elasticsearch', 'enabled');
79
+    }
80
+
81
+    function onPluginVersion(array &$versions)
82
+    {
83
+        $versions[] = array('name' => 'Elasticsearch backend',
84
+                            'version' => self::VERSION,
85
+                            'author' => 'chimo',
86
+                            'homepage' => 'https://github.com/chimo/gs-elasticsearch',
87
+                            'description' =>
88
+                            // TRANS: Plugin description.
89
+                            _m('ElasticSearch engine'));
90
+        return true;
91
+    }
92
+}
93
+

+ 49
- 0
README.md View File

@@ -0,0 +1,49 @@
1
+# Elasticsearch Backend for GNU social
2
+
3
+## Installation
4
+
5
+1. Navigate to your `/local/plugins` directory (create it if it doesn't exist)
6
+1. `git clone https://github.com/chimo/gs-elasticsearch.git ElasticSearch`
7
+1. Run `composer install` in the `ElasticSearch` folder to install the dependencies
8
+
9
+## Configuration
10
+
11
+Tell `/config.php` to use it with (replace `127.0.0.1:9200` with the address/port of your elasticsearch backend server):
12
+
13
+```
14
+    $config['elasticsearch']['enabled'] = true;
15
+    $config['elasticsearch']['hosts'] = [ '127.0.0.1:9200' ];
16
+    $config['elasticsearch']['index_name'] = 'gnusocial';
17
+    addPlugin('ElasticSearch');
18
+```
19
+
20
+## Usage
21
+
22
+You can use the [Lucene query syntax](https://www.elastic.co/guide/en/elasticsearch/reference/5.x/query-dsl-query-string-query.html#query-string-syntax) when searching.
23
+
24
+### Searching Notices
25
+
26
+Supported fields:
27
+
28
+* text: Filters by notice text (default field)
29
+* author: Filters by notice username
30
+
31
+The `/search/notice` page searches notice text by default. You can filter by notice author with the `author` field parameter.
32
+
33
+For example, the following input will find all notices containing the word "social": `social`
34
+
35
+The following input will find all notices containing the word "social" authored by username "gnu": `author:gnu social`
36
+
37
+### Searching Profiles
38
+
39
+Supported fields:
40
+
41
+* nickname (default field)
42
+* fullname
43
+* bio
44
+* location
45
+* created
46
+* modified
47
+
48
+The `/search/people` page searches profile nicknames by default. You can fiter by the other fields above.
49
+

+ 6
- 0
composer.json View File

@@ -0,0 +1,6 @@
1
+{
2
+    "require": {
3
+        "elasticsearch/elasticsearch": "~5.0"
4
+    }
5
+}
6
+

+ 267
- 0
composer.lock View File

@@ -0,0 +1,267 @@
1
+{
2
+    "_readme": [
3
+        "This file locks the dependencies of your project to a known state",
4
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
5
+        "This file is @generated automatically"
6
+    ],
7
+    "content-hash": "176780d3d713a41a474af726de5fb5b4",
8
+    "packages": [
9
+        {
10
+            "name": "elasticsearch/elasticsearch",
11
+            "version": "v5.3.0",
12
+            "source": {
13
+                "type": "git",
14
+                "url": "https://github.com/elastic/elasticsearch-php.git",
15
+                "reference": "50e5b1c63db68839b8acc1f4766769570a27a448"
16
+            },
17
+            "dist": {
18
+                "type": "zip",
19
+                "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/50e5b1c63db68839b8acc1f4766769570a27a448",
20
+                "reference": "50e5b1c63db68839b8acc1f4766769570a27a448",
21
+                "shasum": ""
22
+            },
23
+            "require": {
24
+                "guzzlehttp/ringphp": "~1.0",
25
+                "php": "^5.6|^7.0",
26
+                "psr/log": "~1.0"
27
+            },
28
+            "require-dev": {
29
+                "cpliakas/git-wrapper": "~1.0",
30
+                "doctrine/inflector": "^1.1",
31
+                "mockery/mockery": "0.9.4",
32
+                "phpunit/phpunit": "^4.7|^5.4",
33
+                "sami/sami": "~3.2",
34
+                "symfony/finder": "^2.8",
35
+                "symfony/yaml": "^2.8"
36
+            },
37
+            "suggest": {
38
+                "ext-curl": "*",
39
+                "monolog/monolog": "Allows for client-level logging and tracing"
40
+            },
41
+            "type": "library",
42
+            "autoload": {
43
+                "psr-4": {
44
+                    "Elasticsearch\\": "src/Elasticsearch/"
45
+                }
46
+            },
47
+            "notification-url": "https://packagist.org/downloads/",
48
+            "license": [
49
+                "Apache-2.0"
50
+            ],
51
+            "authors": [
52
+                {
53
+                    "name": "Zachary Tong"
54
+                }
55
+            ],
56
+            "description": "PHP Client for Elasticsearch",
57
+            "keywords": [
58
+                "client",
59
+                "elasticsearch",
60
+                "search"
61
+            ],
62
+            "time": "2017-07-19T18:44:30+00:00"
63
+        },
64
+        {
65
+            "name": "guzzlehttp/ringphp",
66
+            "version": "1.1.0",
67
+            "source": {
68
+                "type": "git",
69
+                "url": "https://github.com/guzzle/RingPHP.git",
70
+                "reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b"
71
+            },
72
+            "dist": {
73
+                "type": "zip",
74
+                "url": "https://api.github.com/repos/guzzle/RingPHP/zipball/dbbb91d7f6c191e5e405e900e3102ac7f261bc0b",
75
+                "reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b",
76
+                "shasum": ""
77
+            },
78
+            "require": {
79
+                "guzzlehttp/streams": "~3.0",
80
+                "php": ">=5.4.0",
81
+                "react/promise": "~2.0"
82
+            },
83
+            "require-dev": {
84
+                "ext-curl": "*",
85
+                "phpunit/phpunit": "~4.0"
86
+            },
87
+            "suggest": {
88
+                "ext-curl": "Guzzle will use specific adapters if cURL is present"
89
+            },
90
+            "type": "library",
91
+            "extra": {
92
+                "branch-alias": {
93
+                    "dev-master": "1.1-dev"
94
+                }
95
+            },
96
+            "autoload": {
97
+                "psr-4": {
98
+                    "GuzzleHttp\\Ring\\": "src/"
99
+                }
100
+            },
101
+            "notification-url": "https://packagist.org/downloads/",
102
+            "license": [
103
+                "MIT"
104
+            ],
105
+            "authors": [
106
+                {
107
+                    "name": "Michael Dowling",
108
+                    "email": "mtdowling@gmail.com",
109
+                    "homepage": "https://github.com/mtdowling"
110
+                }
111
+            ],
112
+            "description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.",
113
+            "time": "2015-05-20T03:37:09+00:00"
114
+        },
115
+        {
116
+            "name": "guzzlehttp/streams",
117
+            "version": "3.0.0",
118
+            "source": {
119
+                "type": "git",
120
+                "url": "https://github.com/guzzle/streams.git",
121
+                "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5"
122
+            },
123
+            "dist": {
124
+                "type": "zip",
125
+                "url": "https://api.github.com/repos/guzzle/streams/zipball/47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5",
126
+                "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5",
127
+                "shasum": ""
128
+            },
129
+            "require": {
130
+                "php": ">=5.4.0"
131
+            },
132
+            "require-dev": {
133
+                "phpunit/phpunit": "~4.0"
134
+            },
135
+            "type": "library",
136
+            "extra": {
137
+                "branch-alias": {
138
+                    "dev-master": "3.0-dev"
139
+                }
140
+            },
141
+            "autoload": {
142
+                "psr-4": {
143
+                    "GuzzleHttp\\Stream\\": "src/"
144
+                }
145
+            },
146
+            "notification-url": "https://packagist.org/downloads/",
147
+            "license": [
148
+                "MIT"
149
+            ],
150
+            "authors": [
151
+                {
152
+                    "name": "Michael Dowling",
153
+                    "email": "mtdowling@gmail.com",
154
+                    "homepage": "https://github.com/mtdowling"
155
+                }
156
+            ],
157
+            "description": "Provides a simple abstraction over streams of data",
158
+            "homepage": "http://guzzlephp.org/",
159
+            "keywords": [
160
+                "Guzzle",
161
+                "stream"
162
+            ],
163
+            "time": "2014-10-12T19:18:40+00:00"
164
+        },
165
+        {
166
+            "name": "psr/log",
167
+            "version": "1.0.2",
168
+            "source": {
169
+                "type": "git",
170
+                "url": "https://github.com/php-fig/log.git",
171
+                "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d"
172
+            },
173
+            "dist": {
174
+                "type": "zip",
175
+                "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
176
+                "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
177
+                "shasum": ""
178
+            },
179
+            "require": {
180
+                "php": ">=5.3.0"
181
+            },
182
+            "type": "library",
183
+            "extra": {
184
+                "branch-alias": {
185
+                    "dev-master": "1.0.x-dev"
186
+                }
187
+            },
188
+            "autoload": {
189
+                "psr-4": {
190
+                    "Psr\\Log\\": "Psr/Log/"
191
+                }
192
+            },
193
+            "notification-url": "https://packagist.org/downloads/",
194
+            "license": [
195
+                "MIT"
196
+            ],
197
+            "authors": [
198
+                {
199
+                    "name": "PHP-FIG",
200
+                    "homepage": "http://www.php-fig.org/"
201
+                }
202
+            ],
203
+            "description": "Common interface for logging libraries",
204
+            "homepage": "https://github.com/php-fig/log",
205
+            "keywords": [
206
+                "log",
207
+                "psr",
208
+                "psr-3"
209
+            ],
210
+            "time": "2016-10-10T12:19:37+00:00"
211
+        },
212
+        {
213
+            "name": "react/promise",
214
+            "version": "v2.5.1",
215
+            "source": {
216
+                "type": "git",
217
+                "url": "https://github.com/reactphp/promise.git",
218
+                "reference": "62785ae604c8d69725d693eb370e1d67e94c4053"
219
+            },
220
+            "dist": {
221
+                "type": "zip",
222
+                "url": "https://api.github.com/repos/reactphp/promise/zipball/62785ae604c8d69725d693eb370e1d67e94c4053",
223
+                "reference": "62785ae604c8d69725d693eb370e1d67e94c4053",
224
+                "shasum": ""
225
+            },
226
+            "require": {
227
+                "php": ">=5.4.0"
228
+            },
229
+            "require-dev": {
230
+                "phpunit/phpunit": "~4.8"
231
+            },
232
+            "type": "library",
233
+            "autoload": {
234
+                "psr-4": {
235
+                    "React\\Promise\\": "src/"
236
+                },
237
+                "files": [
238
+                    "src/functions_include.php"
239
+                ]
240
+            },
241
+            "notification-url": "https://packagist.org/downloads/",
242
+            "license": [
243
+                "MIT"
244
+            ],
245
+            "authors": [
246
+                {
247
+                    "name": "Jan Sorgalla",
248
+                    "email": "jsorgalla@gmail.com"
249
+                }
250
+            ],
251
+            "description": "A lightweight implementation of CommonJS Promises/A for PHP",
252
+            "keywords": [
253
+                "promise",
254
+                "promises"
255
+            ],
256
+            "time": "2017-03-25T12:08:31+00:00"
257
+        }
258
+    ],
259
+    "packages-dev": [],
260
+    "aliases": [],
261
+    "minimum-stability": "stable",
262
+    "stability-flags": [],
263
+    "prefer-stable": false,
264
+    "prefer-lowest": false,
265
+    "platform": [],
266
+    "platform-dev": []
267
+}

+ 156
- 0
lib/ElasticSearch.php View File

@@ -0,0 +1,156 @@
1
+<?php
2
+
3
+if (!defined('GNUSOCIAL')) {
4
+    exit(1);
5
+}
6
+
7
+require(__DIR__ . '/../vendor/autoload.php');
8
+require(INSTALLDIR . '/lib/search_engines.php');
9
+
10
+use Elasticsearch\ClientBuilder;
11
+
12
+class ElasticSearch extends SearchEngine
13
+{
14
+    private $client;
15
+    private $index_name;
16
+    protected $table;
17
+    protected $target;
18
+
19
+    function __construct($target, $table, $index_name, $hosts)
20
+    {
21
+        // We will be searching Notices and Profiles.
22
+        // We want them to be in different indexes, so we suffix the
23
+        // object type to the base 'index name'
24
+        $index_suffix = strtolower(get_class($target));
25
+
26
+        $this->target = $target;
27
+        $this->table = $table;
28
+        $this->index_name = $index_name . '-' . $index_suffix;
29
+        $this->index_type = $index_suffix;
30
+        $this->hosts = $hosts;
31
+
32
+        $this->client = ClientBuilder::create()->setHosts($hosts)->build();
33
+
34
+        // Create index if it doesn't exist
35
+        if (!$this->client->indices()->exists([ 'index' => $this->index_name ])) {
36
+            $response = $this->client->indices()->create([ 'index' => $this->index_name ]);
37
+
38
+            // TODO: Parse response, handle errors
39
+        }
40
+    }
41
+
42
+    function index($object)
43
+    {
44
+        $response = 'Trying to index unsupported object. Aborting.';
45
+
46
+        switch(get_class($object)) {
47
+            case 'Notice':
48
+                $response = $this->indexNotice($object);
49
+                break;
50
+            case 'Profile':
51
+                $response = $this->indexProfile($object);
52
+                break;
53
+            default:
54
+                break;
55
+        }
56
+
57
+        return $response;
58
+    }
59
+
60
+    function indexNotice($notice)
61
+    {
62
+        $author = Profile::getKV('id', $notice->profile_id);
63
+
64
+        $params = [
65
+            'index' => $this->index_name,
66
+            'type' => $this->index_type,
67
+            'id' => $notice->id,
68
+            'body' => [
69
+                'author' => $author->nickname,
70
+                'text' => $notice->content,
71
+                'created' => $notice->created
72
+            ]
73
+        ];
74
+
75
+        return $this->client->index($params);
76
+    }
77
+
78
+    function indexProfile($profile)
79
+    {
80
+        $params = [
81
+            'index' => $this->index_name,
82
+            'type' => $this->index_type,
83
+            'id' => $profile->id,
84
+            'body' => [
85
+                'nickname' => $profile->nickname,
86
+                'fullname' => $profile->fullname,
87
+                'bio' => $profile->bio,
88
+                'location' => $profile->location,
89
+                'created' => $profile->created,
90
+                'modified' => $profile->modified
91
+            ]
92
+        ];
93
+
94
+        $response = $this->client->index($params);
95
+
96
+        // TODO: Parse response, handle errors
97
+    }
98
+
99
+    // From SearchEngine class
100
+    function query($q)
101
+    {
102
+        $default_field = 'text';
103
+
104
+        if (get_class($this->target) === 'Profile') {
105
+            $default_field = 'nickname';
106
+        }
107
+
108
+        $params = [
109
+            'index' => $this->index_name,
110
+            'type' => $this->index_type,
111
+            'body' => [
112
+                'query' => [
113
+                    'query_string' => [
114
+                        'default_field' => $default_field,
115
+                        'query' => $q
116
+                    ]
117
+                ]
118
+            ]
119
+        ];
120
+
121
+        $response = $this->client->search($params);
122
+
123
+        // TODO: Parse response, handle errors
124
+
125
+        $hits = $response['hits']['hits'];
126
+
127
+        if (count($hits) === 0) {
128
+            return false;
129
+        }
130
+
131
+        $ids = array();
132
+
133
+        foreach($hits as $hit) {
134
+            $ids[] = $hit['_id'];
135
+        }
136
+
137
+        $id_set = join(', ', $ids);
138
+
139
+        $this->target->whereAdd("id in ($id_set)");
140
+
141
+        return true;
142
+    }
143
+
144
+    // From SearchEngine class
145
+    function limit($offset, $count, $rss = false)
146
+    {
147
+        // TODO
148
+    }
149
+
150
+    // From SearchEngine class
151
+    function set_sort_mode($mode)
152
+    {
153
+        // TODO
154
+    }
155
+}
156
+

Loading…
Cancel
Save