Skip to content

Commit 6652b97

Browse files
Elasticsearch (#14)
* Installed and setup Laravel packages required for Elasticsearch. Created Elasticsearch migrations for mapping indexes for activities, playlists and projects. Added migrations and seeders for "h5p_elasticsearch_fields" table. Created Model, Repository and Repository Interface for h5p elasticsearch field. Refactored Elasticsearch fields indexing and seed functions from Activity model to Repository. Integrated Elasticsearch indexing for Playlist and Projects models. Refactored and integrated Elasticsearch query builder for Activity, Playlist and Projects. Integrated search for deep linking and advance search. Updated elastic migrations according to new DB schema for Activity, Playlist and Projects. Refactored the search API according to updates suggested by other developers. Refactored resources collections, resources, models, repositories, migrations, seeders and controller as per DB updates. Added post filter for index type in Elasticsearch API. Added first user with role "teacher" as project, playlist and activity user in search listing. Added the API docs for search end points and generated docs. * Added the feature to filter search results by subject_id and education_level_id. * Added "ELASTIC_HOST" and "SCOUT_DRIVER" to .env. * Refactored code for formatting. * Added space after : for each field in api docs. Split validation from search controller as per AuthController login/register method. Deleted H5pContent/H5pLibrary models, switched to the ones inside H5P. Replaced "Activity::searchForm()" with "$this->model". Extended H5pElasticsearchField and H5pLibrary repositories with EloquentRepositoryInterface. Added relation to h5p_library table for h5p_elasticsearch_fields migration. Wrapped "if" with bracket even though there is 1 line inside condition. Removed the Duplicated "shared" from elastic migration for playlists index. Opened brackets in new lines for "getUserAttribute()" and "getModelTypeAttribute()" in Project model. Repaced "shared" in Playlist model with "is_public" field. * Added migrations and checks for "elasticsearch" in activties, playlists and projects. Used elseif instead of else if, so its a one word. Updated added a space before opening bracket. Added brackets even if the block has a single line for "if/else" block. Defined the function in the H5pLibraryRepositoryInterface interface as well. Added space before the opening brace. Used === instead of == where we are doing string comparison. Added space before opening brace where I could find. Placed public functions defined in H5pLibraryRepository and ActivityRepository in their respective interfaces. Added blank line above declare() in activties, playlists and projects elastic migrations. Updated advance search route to "search/advanced". Added single linefeed at the end of SearchFormQueryBuilder.php. Moved all class properties at the top in SearchFormQueryBuilder.php. Replaced $searchRequest by $data in SearchController.php.
1 parent 9b1b76f commit 6652b97

30 files changed

+2111
-711
lines changed

.env.example

+4
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,8 @@ MATTERMOST_DEFAULT_TEAM_ID=
5555

5656
GAPI_CLASSROOM_CREDENTIALS=
5757
GAPI_CLIENT_ID=
58+
5859
CURRIKI_TSUGI_HOST=
60+
61+
ELASTIC_HOST=104.237.136.73:9200
62+
SCOUT_DRIVER=elastic

H5P/laravel-h5p/src/LaravelH5p/Eloquents/H5pLibrary.php

+8
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ public function getCountLibraryDependencies()
5757
return intval($usage['libraries']);
5858
}
5959

60+
/**
61+
* Get the fields for the library.
62+
*/
63+
public function fields()
64+
{
65+
return $this->hasMany('App\Models\H5pElasticsearchField', 'library_id');
66+
}
67+
6068
public function content()
6169
{
6270
return $this->hasOne(H5pContent::class, 'library_id');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\V1;
4+
5+
use App\Http\Controllers\Controller;
6+
use Illuminate\Http\Request;
7+
use App\Http\Requests\SearchRequest;
8+
use Illuminate\Http\Response;
9+
use App\Repositories\Activity\ActivityRepositoryInterface;
10+
use Illuminate\Support\Facades\Validator;
11+
12+
/**
13+
* @authenticated
14+
*
15+
* @group Search API
16+
*
17+
* APIs for search
18+
*/
19+
class SearchController extends Controller
20+
{
21+
private $activityRepository;
22+
23+
/**
24+
* SearchController constructor.
25+
*
26+
* @param ActivityRepositoryInterface $activityRepository
27+
*/
28+
public function __construct(ActivityRepositoryInterface $activityRepository)
29+
{
30+
$this->activityRepository = $activityRepository;
31+
}
32+
33+
/**
34+
* Deep Linking Search
35+
*
36+
* Search projects, playlists and activities for deep linking
37+
*
38+
* @queryParam query required Query to search. Example: test
39+
* @queryParam sort Field to sort by. Example: created_at
40+
* @queryParam order Order to sort by. Example: desc
41+
* @queryParam from Index where the pagination start from. Example: 0
42+
* @queryParam size Number of records to return. Example: 10
43+
*
44+
* @response {
45+
* "projects": {
46+
* "457": {
47+
* "id": 457,
48+
* "name": "Text Structure Lesson 2 Problem and Solution",
49+
* "description": "Learning Objective\nThe learner will define and describe the main characteristics...",
50+
* "thumb_url": "/storage/uploads/3zjZuLoQrRk0MZ5wP7UPyOut5zUybf3tW3a4Q2M1.png",
51+
* "mongo_userid": "5ef5300e41668b53ea5ed1b3",
52+
* "starter_project": false,
53+
* "created_at": "2020-07-17T17:49:01.000000Z",
54+
* "updated_at": "2020-08-06T13:28:07.000000Z",
55+
* "deleted_at": null,
56+
* "playlists": {
57+
* "225": {
58+
* "id": 225,
59+
* "title": "Solving Ratio and Rate Problems",
60+
* "project_id": 64,
61+
* "order": null,
62+
* "mongo_projectid": "5f3ae92a924a1d5ddf44dd80",
63+
* "created_at": null,
64+
* "updated_at": null,
65+
* "deleted_at": null,
66+
* "activities": {
67+
* "993": {
68+
* "id": 993,
69+
* "playlist_id": 225,
70+
* "title": "",
71+
* "type": "h5p",
72+
* "content": "",
73+
* "h5p_content_id": 17474,
74+
* "thumb_url": "/storage/uploads/5f3aedeba24fb.jpeg",
75+
* "subject_id": null,
76+
* "education_level_id": null,
77+
* "shared": false,
78+
* "order": 11,
79+
* "mongo_playlistid": "5f3aeccd924a1d5ddf44ddd0",
80+
* "created_at": null,
81+
* "updated_at": null,
82+
* "deleted_at": null
83+
* }
84+
* }
85+
* }
86+
* }
87+
* }
88+
* }
89+
* }
90+
* @response 400 {
91+
* "errors": {
92+
* "query": [
93+
* "The query field is required."
94+
* ]
95+
* }
96+
* }
97+
* @param SearchRequest $searchRequest
98+
* @return Response
99+
*/
100+
public function search(SearchRequest $searchRequest)
101+
{
102+
$data = $searchRequest->validated();
103+
104+
$projects = $this->activityRepository->searchForm($data);
105+
106+
return response([
107+
'projects' => $projects,
108+
], 200);
109+
}
110+
111+
/**
112+
* Advance search
113+
*
114+
* Advance search for projects, playlists and activities
115+
*
116+
* @queryParam query required Query to search. Example: test
117+
* @queryParam negativeQuery Terms that should not exist. Example: badword
118+
* @queryParam userIds Array of user ids to match. Example: [1]
119+
* @queryParam model Index to filter by. Example: activities
120+
* @queryParam sort Field to sort by. Example: created_at
121+
* @queryParam order Order to sort by. Example: desc
122+
* @queryParam from Index where the pagination start from. Example: 0
123+
* @queryParam size Number of records to return. Example: 10
124+
*
125+
* @apiResourceCollection App\Http\Resources\V1\SearchResource
126+
* @apiResourceModel App\Models\Activity
127+
*
128+
* @response {
129+
* "data": [
130+
* {
131+
* "id": 457,
132+
* "thumb_url": "/storage/uploads/3zjZuLoQrRk0MZ5wP7UPyOut5zUybf3tW3a4Q2M1.png",
133+
* "title": "Text Structure Lesson 2 Problem and Solution",
134+
* "description": "Learning Objective\nThe learner will define and describe the main characteristics...",
135+
* "model": "Project",
136+
* "user": {
137+
* "id": 1,
138+
* "name": "localuser",
139+
* "email": "[email protected]",
140+
* "email_verified_at": null,
141+
* "created_at": "2020-08-22T12:13:52.000000Z",
142+
* "updated_at": "2020-08-22T12:13:52.000000Z",
143+
* "first_name": "test",
144+
* "last_name": "test",
145+
* "organization_name": "organization_name",
146+
* "job_title": "job_title",
147+
* "address": null,
148+
* "phone_number": null,
149+
* "organization_type": null,
150+
* "website": null,
151+
* "deleted_at": null,
152+
* "role": null,
153+
* "gapi_access_token": null,
154+
* "pivot": {
155+
* "project_id": 457,
156+
* "user_id": 1,
157+
* "role": "teacher",
158+
* "created_at": "2020-08-25T09:35:35.000000Z",
159+
* "updated_at": "2020-08-25T09:35:35.000000Z"
160+
* }
161+
* }
162+
* },
163+
* {
164+
* "id": 1102,
165+
* "title": "All About That Text",
166+
* "model": "Playlist",
167+
* "user": null
168+
* }
169+
* ],
170+
* "meta": {
171+
* "projects": 7,
172+
* "playlists": 3,
173+
* "activities": 2,
174+
* "total": 12
175+
* }
176+
* }
177+
* @response 400 {
178+
* "errors": {
179+
* "userIds": [
180+
* "The user ids must be an array."
181+
* ]
182+
* }
183+
* }
184+
*
185+
* @param SearchRequest $searchRequest
186+
* @return Response
187+
*/
188+
public function advance(SearchRequest $searchRequest)
189+
{
190+
$data = $searchRequest->validated();
191+
192+
$results = $this->activityRepository->advanceSearchForm($data);
193+
194+
return $results;
195+
}
196+
}

app/Http/Requests/SearchRequest.php

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace App\Http\Requests;
4+
5+
use Illuminate\Foundation\Http\FormRequest;
6+
7+
class SearchRequest extends FormRequest
8+
{
9+
/**
10+
* Determine if the user is authorized to make this request.
11+
*
12+
* @return bool
13+
*/
14+
public function authorize()
15+
{
16+
return true;
17+
}
18+
19+
/**
20+
* Get the validation rules that apply to the request.
21+
*
22+
* @return array
23+
*/
24+
public function rules()
25+
{
26+
return [
27+
'query' => 'required|string|max:255',
28+
'negativeQuery' => 'string|max:255',
29+
'userIds' => 'array|exists:App\User,id',
30+
'subjectIds' => 'array|exists:App\Models\Activity,subject_id',
31+
'educationLevelIds' => 'array|exists:App\Models\Activity,education_level_id',
32+
'model' => 'in:activities,playlists,projects',
33+
'sort' => 'in:created_at',
34+
'order' => 'in:asc,desc',
35+
'from' => 'integer',
36+
'size' => 'integer'
37+
];
38+
}
39+
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Http\Resources\V1;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Http\Resources\Json\JsonResource;
7+
use App\Http\Resources\V1\UserResource;
8+
9+
class SearchResource extends JsonResource
10+
{
11+
/**
12+
* Transform the resource into an array.
13+
*
14+
* @param Request $request
15+
* @return array
16+
*/
17+
public function toArray($request)
18+
{
19+
return [
20+
'id' => $this->id,
21+
'thumb_url' => $this->when(isset($this->thumb_url), $this->thumb_url),
22+
'title' => (isset($this->title) ? $this->title : $this->name ),
23+
'description' => $this->when(isset($this->description), $this->description),
24+
'model' => $this->modelType,
25+
'user' => new UserResource($this->user)
26+
];
27+
}
28+
}

app/Models/Activity.php

+68-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44

55
use Illuminate\Database\Eloquent\Model;
66
use Illuminate\Database\Eloquent\SoftDeletes;
7+
use Laravel\Scout\Searchable;
8+
use ElasticScoutDriverPlus\CustomSearch;
9+
use ElasticScoutDriverPlus\Builders\SearchRequestBuilder;
10+
use App\Models\QueryBuilders\SearchFormQueryBuilder;
11+
use App\Repositories\Activity\ActivityRepositoryInterface;
712

813
class Activity extends Model
914
{
10-
use SoftDeletes;
15+
use SoftDeletes, Searchable, CustomSearch;
1116

1217
/**
1318
* The attributes that are mass assignable.
@@ -25,8 +30,46 @@ class Activity extends Model
2530
'subject_id',
2631
'education_level_id',
2732
'h5p_content_id',
33+
'elasticsearch',
2834
];
2935

36+
/**
37+
* Get the attributes to be indexed in Elasticsearch
38+
*/
39+
public function toSearchableArray()
40+
{
41+
$searchableArray = [
42+
'playlist_id' => $this->playlist_id,
43+
'title' => $this->title,
44+
'type' => $this->type,
45+
'content' => $this->content,
46+
'h5p_content_id' => $this->h5p_content_id,
47+
'subject_id' => $this->subject_id,
48+
'education_level_id' => $this->education_level_id,
49+
'is_public' => $this->is_public,
50+
'elasticsearch' => $this->elasticsearch,
51+
'created_at' => $this->created_at ? $this->created_at->toAtomString() : null,
52+
'updated_at' => $this->updated_at ? $this->updated_at->toAtomString() : null
53+
];
54+
55+
if ($this->playlist) {
56+
$searchableArray['project_id'] = $this->playlist->project_id;
57+
}
58+
59+
$activityRepository = resolve(ActivityRepositoryInterface::class);
60+
$searchableArray = $searchableArray + $activityRepository->getH5pElasticsearchFields($this->h5p_content);
61+
62+
return $searchableArray;
63+
}
64+
65+
/**
66+
* Get the search request
67+
*/
68+
public static function searchForm(): SearchRequestBuilder
69+
{
70+
return new SearchRequestBuilder(new static(), new SearchFormQueryBuilder());
71+
}
72+
3073
/**
3174
* Get the playlist that owns the activity
3275
*/
@@ -35,6 +78,30 @@ public function playlist()
3578
return $this->belongsTo('App\Models\Playlist', 'playlist_id');
3679
}
3780

81+
/**
82+
* Get the activity's project's user.
83+
*
84+
* @return object
85+
*/
86+
public function getUserAttribute()
87+
{
88+
if (isset($this->playlist) && isset($this->playlist->project) && isset($this->playlist->project->users)) {
89+
return $this->playlist->project->users()->wherePivot('role', 'teacher')->first();
90+
}
91+
92+
return null;
93+
}
94+
95+
/**
96+
* Get the model type.
97+
*
98+
* @return string
99+
*/
100+
public function getModelTypeAttribute()
101+
{
102+
return 'Activity';
103+
}
104+
38105
/**
39106
* Get the H5P Content relation
40107
*/

0 commit comments

Comments
 (0)