- PopularZola2020-03-22T00:00:00+00:00https://www.fullstackstanley.com/tags/popular/atom.xmlReplicating the MacOS Search TextField in SwiftUI2020-03-22T00:00:00+00:002020-03-22T00:00:00+00:00https://www.fullstackstanley.com/articles/replicating-the-macos-search-textfield-in-swiftui/<p>I wanted to add a search text field to a MacOS app that I'm working on and soon discovered it's not available in SwiftUI (as off 5.1). I've put together a quick replication which mostly works, but unfortunately involves some hackiness.</p>
<span id="continue-reading"></span>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/i/uje3uwqxxogogpex9d5e.gif" alt="Preview of Search TextFIeld" /></p>
<p>Here's the code:</p>
<pre data-lang="swift" style="background-color:#2b303b;color:#c0c5ce;" class="language-swift "><code class="language-swift" data-lang="swift"><span style="color:#65737e;">// This extension removes the focus ring entirely.
</span><span style="color:#b48ead;">extension</span><span> NSTextField {
</span><span> </span><span style="color:#b48ead;">open override var</span><span> focusRingType: NSFocusRingType {
</span><span> </span><span style="color:#b48ead;">get</span><span> { .</span><span style="color:#b48ead;">none</span><span> }
</span><span> </span><span style="color:#b48ead;">set</span><span> { }
</span><span> }
</span><span>}
</span><span>
</span><span style="color:#b48ead;">struct</span><span> SearchTextField: View {
</span><span> </span><span style="color:#b48ead;">@Binding var</span><span> query: String
</span><span> </span><span style="color:#b48ead;">@State var</span><span> isFocused: Bool = </span><span style="color:#b48ead;">false
</span><span> </span><span style="color:#b48ead;">var</span><span> placeholder: String = </span><span style="color:#a3be8c;">"Search..."
</span><span> </span><span style="color:#b48ead;">var</span><span> body: some View {
</span><span> ZStack {
</span><span> RoundedRectangle(cornerRadius: </span><span style="color:#d08770;">5</span><span>, style: .continuous)
</span><span> .fill(Color.white)
</span><span> .frame(width: </span><span style="color:#d08770;">200</span><span>, height: </span><span style="color:#d08770;">22</span><span>)
</span><span> .overlay(
</span><span> RoundedRectangle(cornerRadius: </span><span style="color:#d08770;">5</span><span>, style: .continuous)
</span><span> .stroke(isFocused ? Color.blue.opacity(</span><span style="color:#d08770;">0</span><span>.</span><span style="color:#d08770;">7</span><span>) : Color.gray.opacity(</span><span style="color:#d08770;">0</span><span>.</span><span style="color:#d08770;">4</span><span>), lineWidth: isFocused ? </span><span style="color:#d08770;">3 </span><span>: </span><span style="color:#d08770;">1</span><span>)
</span><span> .frame(width: </span><span style="color:#d08770;">200</span><span>, height: </span><span style="color:#d08770;">21</span><span>)
</span><span> )
</span><span>
</span><span> HStack {
</span><span> Image(</span><span style="color:#a3be8c;">"magnifyingglass"</span><span>).resizable().aspectRatio(contentMode: .fill)
</span><span> .frame(width:</span><span style="color:#d08770;">12</span><span>, height: </span><span style="color:#d08770;">12</span><span>)
</span><span> .padding(.leading, </span><span style="color:#d08770;">5</span><span>)
</span><span> .opacity(</span><span style="color:#d08770;">0</span><span>.</span><span style="color:#d08770;">8</span><span>)
</span><span> TextField(placeholder, text: $query, onEditingChanged: { (editingChanged) </span><span style="color:#b48ead;">in
</span><span> </span><span style="color:#b48ead;">if</span><span> editingChanged {
</span><span> </span><span style="color:#b48ead;">self</span><span>.isFocused = </span><span style="color:#b48ead;">true
</span><span> } </span><span style="color:#b48ead;">else</span><span> {
</span><span> </span><span style="color:#b48ead;">self</span><span>.isFocused = </span><span style="color:#b48ead;">false
</span><span> }
</span><span> })
</span><span> .textFieldStyle(PlainTextFieldStyle())
</span><span> </span><span style="color:#b48ead;">if</span><span> query != </span><span style="color:#a3be8c;">""</span><span> {
</span><span> Button(action: {
</span><span> </span><span style="color:#b48ead;">self</span><span>.query = </span><span style="color:#a3be8c;">""
</span><span> }) {
</span><span> Image(</span><span style="color:#a3be8c;">"xmark.circle.fill"</span><span>)
</span><span> .resizable()
</span><span> .aspectRatio(contentMode: .fit)
</span><span> .frame(width:</span><span style="color:#d08770;">14</span><span>, height: </span><span style="color:#d08770;">14</span><span>)
</span><span> .padding(.trailing, </span><span style="color:#d08770;">3</span><span>)
</span><span> .opacity(</span><span style="color:#d08770;">0</span><span>.</span><span style="color:#d08770;">5</span><span>)
</span><span> }
</span><span> .buttonStyle(PlainButtonStyle())
</span><span> .opacity(</span><span style="color:#b48ead;">self</span><span>.query == </span><span style="color:#a3be8c;">"" </span><span>? </span><span style="color:#d08770;">0 </span><span>: </span><span style="color:#d08770;">0</span><span>.</span><span style="color:#d08770;">5</span><span>)
</span><span> }
</span><span> }
</span><span>
</span><span> }
</span><span> }
</span><span>}
</span></code></pre>
<p>Here's how you use it:</p>
<pre data-lang="swift" style="background-color:#2b303b;color:#c0c5ce;" class="language-swift "><code class="language-swift" data-lang="swift"><span>SearchTextField(query: $searchQuery, placeholder: </span><span style="color:#a3be8c;">"Search..."</span><span>)
</span><span> .frame(minWidth: </span><span style="color:#d08770;">60</span><span>.</span><span style="color:#d08770;">0</span><span>, idealWidth: </span><span style="color:#d08770;">200</span><span>.</span><span style="color:#d08770;">0</span><span>, maxWidth: </span><span style="color:#d08770;">200</span><span>.</span><span style="color:#d08770;">0</span><span>, minHeight: </span><span style="color:#d08770;">24</span><span>.</span><span style="color:#d08770;">0</span><span>, idealHeight: </span><span style="color:#d08770;">21</span><span>.</span><span style="color:#d08770;">0</span><span>, maxHeight: </span><span style="color:#d08770;">21</span><span>.</span><span style="color:#d08770;">0</span><span>, alignment: .center)
</span></code></pre>
<h2 id="caveats">Caveats</h2>
<p>There are a few issues with this solution. Hopefully in a future version of SwiftUI this will be fixed.</p>
<h3 id="removing-the-focus-ring-from-all-textfields">Removing the Focus Ring from All TextFields</h3>
<p>This code creates a pretend TextField which holds the magnifying glass, a real TextField, and the close icon inside of it. If you remove the NSTextField extension you'll see the real textfield when focused. It's currently not possible to disable a specific focus ring so you'll have to reimplement them if you need to show them.</p>
<h3 id="fake-focus-ring-doesn-t-update-on-focus">Fake Focus Ring Doesn't Update on Focus</h3>
<p>You'll notice in the GIF above that the focus ring does not show until you start typing. Not ideal but usable. If anyone has a suggestion for fixing this I'd be happy to know!</p>
<h3 id="no-sf-symbols-in-macos">No SF Symbols in MacOS</h3>
<p>Surprisingly, MacOS does not have native support for SF Symbols! I used <a href="https://github.com/knezzy/sfsymbols">this tool</a> to convert them to PNG and import them into my assets catalog. The symbols used are <code>xmark.circle.fill</code> and <code>magnifyingglass</code>.</p>
Simple Search with Laravel and ElasticSearch2015-01-10T00:00:00+00:002015-01-10T00:00:00+00:00https://www.fullstackstanley.com/articles/simple-search-with-laravel-and-elasticsearch/<p>I was recently asked to make a search engine for a client's website. Normally I would go down the MySQL fulltext search route but I was feeling rather adventurous at the time. I had no experience with ElasticSearch, Apache Solr or any other search system prior to this so I decided to pick ElasticSearch and dive in head first. This tutorial is a result of some of the things I picked up while learning it.</p>
<p>I aim to show you how to set up the <a href="https://github.com/adamfairholm/Elasticquent">Elasticquent</a> Laravel package and some basic ways to fine tune your search engine.</p>
<span id="continue-reading"></span>
<p><strong>Note:</strong> This tutorial is aimed at developers who are already familiar with Laravel but are new to ElasticSearch and want some guidance on getting them to work together.</p>
<h2 id="installing-elasticsearch">Installing ElasticSearch</h2>
<p>If you haven't installed ElasticSearch then make sure you check the <a href="http://www.elasticsearch.org/guide/en/elasticsearch/guide/current/_installing_elasticsearch.html">ElasticSearch documentation</a> for setting it up. Although not necessary it's worth running through the rest of the getting started guide to understand the basics of how ElasticSearch works.</p>
<p>I also recommend <a href="https://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm?hl=en">Postman App</a> if you use Chrome. Postman will let you run REST commands in a nice GUI rather than using command line.</p>
<p>Confirm that your ElasticSearch instance is running by the following command in your command line / Postman App.</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>curl -XPOST 'http://localhost:9200/?pretty'
</span></code></pre>
<p>You should see a nice prettified json response if everything is working okay.</p>
<pre data-lang="json" style="background-color:#2b303b;color:#c0c5ce;" class="language-json "><code class="language-json" data-lang="json"><span>{
</span><span> "</span><span style="color:#a3be8c;">status</span><span>": </span><span style="color:#d08770;">200</span><span>,
</span><span> "</span><span style="color:#a3be8c;">name</span><span>": "</span><span style="color:#a3be8c;">Mystique</span><span>",
</span><span> "</span><span style="color:#a3be8c;">cluster_name</span><span>": "</span><span style="color:#a3be8c;">elasticsearch_mitch</span><span>",
</span><span> "</span><span style="color:#a3be8c;">version</span><span>": {
</span><span> "</span><span style="color:#a3be8c;">...</span><span>"
</span><span> },
</span><span> "</span><span style="color:#a3be8c;">tagline</span><span>": "</span><span style="color:#a3be8c;">You Know, for Search</span><span>"
</span><span>}
</span></code></pre>
<h2 id="setting-up-laravel">Setting up Laravel</h2>
<p>I have a fresh Laravel App up and running with a MySQL database. If you are unfamiliar see the <a href="http://laravel.com/docs/4.2/installation">Laravel docs on installing</a>.</p>
<h2 id="packages">Packages</h2>
<p>We'll be using 2 packages in this tutorial, Elasticquent and Faker. You can ignore faker if you plan on importing your own data. For the sake of the tutorial I'll include it.</p>
<p>Open composer.json and add the following to the <code>require</code> object:</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>"fairholm/elasticquent": "1.0.*",
</span><span>"fzaninotto/Faker": "1.4.*"
</span></code></pre>
<p>Run <code>composer update --prefer-dist</code> and we'll create our database table.</p>
<h2 id="generating-our-database-table">Generating our database table</h2>
<p>We're going to set up a "posts" table with 3 fields: title, content and tags.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">php</span><span> artisan migrate:make create_posts_table
</span></code></pre>
<p>In your migration file (located in <code>app/db/migrations/<DATETIME>_create_posts_table.php</code>) use the following code:</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php
</span><span>
</span><span style="color:#b48ead;">use </span><span>Illuminate\Database\Migrations\</span><span style="color:#ebcb8b;">Migration</span><span>;
</span><span style="color:#b48ead;">use </span><span>Illuminate\Database\Schema\</span><span style="color:#ebcb8b;">Blueprint</span><span>;
</span><span>
</span><span style="color:#b48ead;">class </span><span style="color:#ebcb8b;">CreatePostsTable </span><span style="color:#b48ead;">extends </span><span style="color:#a3be8c;">Migration </span><span style="color:#eff1f5;">{
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#65737e;">/**
</span><span style="color:#65737e;"> * Run the migrations.
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> * </span><span style="color:#b48ead;">@return</span><span style="color:#65737e;"> void
</span><span style="color:#65737e;"> */
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">public function </span><span style="color:#8fa1b3;">up</span><span style="color:#eff1f5;">()
</span><span style="color:#eff1f5;"> {
</span><span style="color:#eff1f5;"> </span><span style="color:#ebcb8b;">Schema</span><span style="color:#eff1f5;">::</span><span style="color:#bf616a;">create</span><span style="color:#eff1f5;">(</span><span>'</span><span style="color:#a3be8c;">posts</span><span>'</span><span style="color:#eff1f5;">, </span><span style="color:#b48ead;">function</span><span style="color:#eff1f5;">(</span><span style="color:#ebcb8b;">Blueprint </span><span>$</span><span style="color:#bf616a;">table</span><span style="color:#eff1f5;">)
</span><span style="color:#eff1f5;"> {
</span><span style="color:#eff1f5;"> </span><span>$</span><span style="color:#bf616a;">table</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">increments</span><span style="color:#eff1f5;">(</span><span>'</span><span style="color:#a3be8c;">id</span><span>'</span><span style="color:#eff1f5;">);
</span><span style="color:#eff1f5;"> </span><span>$</span><span style="color:#bf616a;">table</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">string</span><span style="color:#eff1f5;">(</span><span>'</span><span style="color:#a3be8c;">title</span><span>'</span><span style="color:#eff1f5;">);
</span><span style="color:#eff1f5;"> </span><span>$</span><span style="color:#bf616a;">table</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">text</span><span style="color:#eff1f5;">(</span><span>'</span><span style="color:#a3be8c;">content</span><span>'</span><span style="color:#eff1f5;">);
</span><span style="color:#eff1f5;"> </span><span>$</span><span style="color:#bf616a;">table</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">string</span><span style="color:#eff1f5;">(</span><span>'</span><span style="color:#a3be8c;">tags</span><span>'</span><span style="color:#eff1f5;">);
</span><span style="color:#eff1f5;"> </span><span>$</span><span style="color:#bf616a;">table</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">timestamps</span><span style="color:#eff1f5;">();
</span><span style="color:#eff1f5;"> });
</span><span style="color:#eff1f5;"> }
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#65737e;">/**
</span><span style="color:#65737e;"> * Reverse the migrations.
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> * </span><span style="color:#b48ead;">@return</span><span style="color:#65737e;"> void
</span><span style="color:#65737e;"> */
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">public function </span><span style="color:#8fa1b3;">down</span><span style="color:#eff1f5;">()
</span><span style="color:#eff1f5;"> {
</span><span style="color:#eff1f5;"> </span><span style="color:#ebcb8b;">Schema</span><span style="color:#eff1f5;">::</span><span style="color:#bf616a;">drop</span><span style="color:#eff1f5;">(</span><span>'</span><span style="color:#a3be8c;">posts</span><span>'</span><span style="color:#eff1f5;">);
</span><span style="color:#eff1f5;"> }
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;">}
</span></code></pre>
<p>Do a migration and then we'll move on to generating some dummy data.</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>php artisan migrate
</span></code></pre>
<h2 id="dummy-data">Dummy data</h2>
<p>We're going to set up a bunch of test data in a seeder file. This will let us test that the search works without the hassle of inserting data manually.</p>
<p>Our initial post model should look like this <code>app/model/Post.php</code></p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php
</span><span>
</span><span style="color:#b48ead;">class </span><span style="color:#ebcb8b;">Post </span><span style="color:#b48ead;">extends </span><span style="color:#a3be8c;">Eloquent </span><span style="color:#eff1f5;">{
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">public </span><span>$</span><span style="color:#bf616a;">fillable </span><span>= </span><span style="color:#eff1f5;">[</span><span>'</span><span style="color:#a3be8c;">title</span><span>'</span><span style="color:#eff1f5;">, </span><span>'</span><span style="color:#a3be8c;">content</span><span>'</span><span style="color:#eff1f5;">, </span><span>'</span><span style="color:#a3be8c;">tags</span><span>'</span><span style="color:#eff1f5;">];
</span><span style="color:#eff1f5;">}
</span></code></pre>
<p>Create <code>app/database/seeds/PostsTableSeeder.php</code> and add the following code</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php
</span><span>
</span><span style="color:#b48ead;">class </span><span style="color:#ebcb8b;">PostsTableSeeder </span><span style="color:#b48ead;">extends </span><span style="color:#a3be8c;">Seeder </span><span style="color:#eff1f5;">{
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">public function </span><span style="color:#8fa1b3;">run</span><span style="color:#eff1f5;">()
</span><span style="color:#eff1f5;"> {
</span><span style="color:#eff1f5;"> </span><span style="color:#65737e;">// Remove any existing data
</span><span style="color:#eff1f5;"> </span><span style="color:#ebcb8b;">DB</span><span style="color:#eff1f5;">::</span><span style="color:#bf616a;">table</span><span style="color:#eff1f5;">(</span><span>'</span><span style="color:#a3be8c;">pages</span><span>'</span><span style="color:#eff1f5;">)-></span><span style="color:#bf616a;">truncate</span><span style="color:#eff1f5;">();
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span>$</span><span style="color:#bf616a;">faker </span><span>= </span><span style="color:#eff1f5;">Faker\</span><span style="color:#ebcb8b;">Factory</span><span style="color:#eff1f5;">::</span><span style="color:#bf616a;">create</span><span style="color:#eff1f5;">();
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#65737e;">// Generate some dummy data
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">for</span><span style="color:#eff1f5;">(</span><span>$</span><span style="color:#bf616a;">i</span><span>=</span><span style="color:#d08770;">0</span><span style="color:#eff1f5;">; </span><span>$</span><span style="color:#bf616a;">i</span><span><</span><span style="color:#d08770;">30</span><span style="color:#eff1f5;">; </span><span>$</span><span style="color:#bf616a;">i</span><span>++</span><span style="color:#eff1f5;">) {
</span><span style="color:#eff1f5;"> </span><span style="color:#ebcb8b;">Post</span><span style="color:#eff1f5;">::</span><span style="color:#bf616a;">create</span><span style="color:#eff1f5;">([
</span><span style="color:#eff1f5;"> </span><span>'</span><span style="color:#a3be8c;">title</span><span>' => $</span><span style="color:#bf616a;">faker</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">sentence</span><span style="color:#eff1f5;">(</span><span style="color:#d08770;">3</span><span style="color:#eff1f5;">),
</span><span style="color:#eff1f5;"> </span><span>'</span><span style="color:#a3be8c;">content</span><span>' => $</span><span style="color:#bf616a;">faker</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">paragraph</span><span style="color:#eff1f5;">(</span><span style="color:#d08770;">5</span><span style="color:#eff1f5;">),
</span><span style="color:#eff1f5;"> </span><span>'</span><span style="color:#a3be8c;">tags</span><span>' => </span><span style="color:#96b5b4;">join</span><span style="color:#eff1f5;">(</span><span>'</span><span style="color:#a3be8c;">,</span><span>'</span><span style="color:#eff1f5;">, </span><span>$</span><span style="color:#bf616a;">faker</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">words</span><span style="color:#eff1f5;">(</span><span style="color:#d08770;">5</span><span style="color:#eff1f5;">))
</span><span style="color:#eff1f5;"> ]);
</span><span style="color:#eff1f5;"> }
</span><span style="color:#eff1f5;"> }
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;">}
</span></code></pre>
<p>After running <code>php artisan db:seed --class="PostsTableSeeder"</code> we should now have plenty of test data to work with!</p>
<h2 id="setting-up-elasticquent">Setting up Elasticquent</h2>
<p>Let's edit our model in <code>app/models/Post.php</code> and add the following:</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php
</span><span style="color:#b48ead;">use </span><span>Elasticquent\</span><span style="color:#ebcb8b;">ElasticquentTrait</span><span>;
</span><span>
</span><span style="color:#b48ead;">class </span><span style="color:#ebcb8b;">Post </span><span style="color:#b48ead;">extends </span><span style="color:#a3be8c;">\Eloquent </span><span style="color:#eff1f5;">{
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">use </span><span style="color:#a3be8c;">ElasticquentTrait</span><span style="color:#eff1f5;">;
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">public </span><span>$</span><span style="color:#bf616a;">fillable </span><span>= </span><span style="color:#eff1f5;">[</span><span>'</span><span style="color:#a3be8c;">title</span><span>'</span><span style="color:#eff1f5;">, </span><span>'</span><span style="color:#a3be8c;">content</span><span>'</span><span style="color:#eff1f5;">, </span><span>'</span><span style="color:#a3be8c;">tags</span><span>'</span><span style="color:#eff1f5;">];
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">protected </span><span>$</span><span style="color:#bf616a;">mappingProperties </span><span>= </span><span style="color:#96b5b4;">array</span><span style="color:#eff1f5;">(
</span><span style="color:#eff1f5;"> </span><span>'</span><span style="color:#a3be8c;">title</span><span>' => </span><span style="color:#eff1f5;">[
</span><span style="color:#eff1f5;"> </span><span>'</span><span style="color:#a3be8c;">type</span><span>' => '</span><span style="color:#a3be8c;">string</span><span>'</span><span style="color:#eff1f5;">,
</span><span style="color:#eff1f5;"> </span><span>"</span><span style="color:#a3be8c;">analyzer</span><span>" => "</span><span style="color:#a3be8c;">standard</span><span>"</span><span style="color:#eff1f5;">,
</span><span style="color:#eff1f5;"> ],
</span><span style="color:#eff1f5;"> </span><span>'</span><span style="color:#a3be8c;">content</span><span>' => </span><span style="color:#eff1f5;">[
</span><span style="color:#eff1f5;"> </span><span>'</span><span style="color:#a3be8c;">type</span><span>' => '</span><span style="color:#a3be8c;">string</span><span>'</span><span style="color:#eff1f5;">,
</span><span style="color:#eff1f5;"> </span><span>"</span><span style="color:#a3be8c;">analyzer</span><span>" => "</span><span style="color:#a3be8c;">standard</span><span>"</span><span style="color:#eff1f5;">,
</span><span style="color:#eff1f5;"> ],
</span><span style="color:#eff1f5;"> </span><span>'</span><span style="color:#a3be8c;">tags</span><span>' => </span><span style="color:#eff1f5;">[
</span><span style="color:#eff1f5;"> </span><span>'</span><span style="color:#a3be8c;">type</span><span>' => '</span><span style="color:#a3be8c;">string</span><span>'</span><span style="color:#eff1f5;">,
</span><span style="color:#eff1f5;"> </span><span>"</span><span style="color:#a3be8c;">analyzer</span><span>" => "</span><span style="color:#a3be8c;">stop</span><span>"</span><span style="color:#eff1f5;">,
</span><span style="color:#eff1f5;"> </span><span>"</span><span style="color:#a3be8c;">stopwords</span><span>" => </span><span style="color:#eff1f5;">[</span><span>"</span><span style="color:#a3be8c;">,</span><span>"</span><span style="color:#eff1f5;">]
</span><span style="color:#eff1f5;"> ],
</span><span style="color:#eff1f5;"> );
</span><span style="color:#eff1f5;">}
</span><span>
</span></code></pre>
<p>On line 2 we create the Elasticquent Trait shortcut and on line 5 we include it in our class.</p>
<p>Line 9 we add our mapping configuration for ElasticSearch. You can read more about mappings <a href="http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping.html">here</a>.</p>
<p>Each mapping has a type and an analyzer. Type's can be various data types including strings, numbers and dates. For now we will stick to the string type but be aware that different types allow you to take advantage of different things. You can learn more about the types that ElasticSearch supports <a href="http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-types.html">here</a></p>
<p>The analyzer determines how ElasticSearch stores your data for searching. I've chosen <code>standard</code> for title and content and <code>stop</code> for tags. The standard analyzer will remove HTML and grammar and index each word separately. The stop analyzer can be set to choose which characters split the words for indexing.</p>
<p>As an example take this sentence:</p>
<blockquote>
<p>I love laravel, ElasticSearch and Laravel work well together.</p>
</blockquote>
<p>With a standard analyzer ElasticSearch will create a list like this:</p>
<ul>
<li>i</li>
<li>love</li>
<li>laravel,</li>
<li>elasticsearch</li>
<li>and</li>
<li>laravel</li>
<li>work</li>
<li>well</li>
<li>together</li>
</ul>
<p>With our settings the stop analyzer will group them like this:</p>
<ul>
<li>I love laravel</li>
<li>ElasticSearch and Laravel work well together.</li>
</ul>
<p>This can be advantagous if you want to prioritise certain phrases.</p>
<p>Now we've configured how we want our search to operate it's time to index our database!</p>
<p>Let's use Laravel's REPL to generate our ElasticSearch data. Go to your command line and type </p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">php</span><span> artisan tinker
</span></code></pre>
<p>Type the following commands</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span>Post::createIndex($shards = null, $replicas = null);
</span><span>
</span><span>Post::putMapping($ignoreConflicts = true);
</span><span>
</span><span>Post::addAllToIndex();
</span></code></pre>
<p>The first command sets up our index. An index is sort of like a database table in the ElasticSearch world.</p>
<p><code>putMapping()</code> takes the mapping properties we set in the model so that ElasticSearch knows how to index all of our data.</p>
<p><code>addAllToIndex()</code> takes all the data from the database and puts it into ElasticSearch</p>
<h2 id="useful-elasticsearch-api-methods">Useful ElasticSearch API methods</h2>
<p>Elasticquent sets up our index as "default" by default. We can view our mappings by using the following curl request</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">curl</span><span> localhost:9200/default/_mapping?pretty
</span></code></pre>
<pre data-lang="json" style="background-color:#2b303b;color:#c0c5ce;" class="language-json "><code class="language-json" data-lang="json"><span>{
</span><span> "</span><span style="color:#a3be8c;">default</span><span>" : {
</span><span> "</span><span style="color:#a3be8c;">mappings</span><span>" : {
</span><span> "</span><span style="color:#a3be8c;">posts</span><span>" : {
</span><span> "</span><span style="color:#a3be8c;">properties</span><span>" : {
</span><span> "</span><span style="color:#a3be8c;">content</span><span>" : {
</span><span> "</span><span style="color:#a3be8c;">type</span><span>" : "</span><span style="color:#a3be8c;">string</span><span>",
</span><span> "</span><span style="color:#a3be8c;">analyzer</span><span>" : "</span><span style="color:#a3be8c;">standard</span><span>"
</span><span> },
</span><span> "</span><span style="color:#a3be8c;">created_at</span><span>" : {
</span><span> "</span><span style="color:#a3be8c;">type</span><span>" : "</span><span style="color:#a3be8c;">string</span><span>"
</span><span> },
</span><span> "</span><span style="color:#a3be8c;">id</span><span>" : {
</span><span> "</span><span style="color:#a3be8c;">type</span><span>" : "</span><span style="color:#a3be8c;">long</span><span>"
</span><span> },
</span><span> "</span><span style="color:#a3be8c;">tags</span><span>" : {
</span><span> "</span><span style="color:#a3be8c;">type</span><span>" : "</span><span style="color:#a3be8c;">string</span><span>",
</span><span> "</span><span style="color:#a3be8c;">analyzer</span><span>" : "</span><span style="color:#a3be8c;">stop</span><span>"
</span><span> },
</span><span> "</span><span style="color:#a3be8c;">title</span><span>" : {
</span><span> "</span><span style="color:#a3be8c;">type</span><span>" : "</span><span style="color:#a3be8c;">string</span><span>",
</span><span> "</span><span style="color:#a3be8c;">analyzer</span><span>" : "</span><span style="color:#a3be8c;">standard</span><span>"
</span><span> },
</span><span> "</span><span style="color:#a3be8c;">updated_at</span><span>" : {
</span><span> "</span><span style="color:#a3be8c;">type</span><span>" : "</span><span style="color:#a3be8c;">string</span><span>"
</span><span> }
</span><span> }
</span><span> }
</span><span> }
</span><span> }
</span><span>}
</span></code></pre>
<p>In ElasticSearch a table is called a type. We can view all of the documents in a specific type with this query:</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>curl 'localhost:9200/default/posts/_search?pretty'
</span></code></pre>
<p>We can do a basic do a basic search by altering the above command slightly</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">curl </span><span>'</span><span style="color:#a3be8c;">localhost:9200/default/posts/_search?q=title:searchterm&pretty</span><span>'
</span></code></pre>
<p>And we can view a specific document with this</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">curl </span><span>'</span><span style="color:#a3be8c;">localhost:9200/default/posts/1?pretty</span><span>'
</span></code></pre>
<p>For more info on the ElasticSearch API check out the <a href="http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs.html">documentation</a></p>
<h2 id="creating-the-front-end">Creating the front end</h2>
<p>Add a route to <code>app/routes.php</code></p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php
</span><span style="color:#ebcb8b;">Route</span><span>::</span><span style="color:#bf616a;">get</span><span>('</span><span style="color:#a3be8c;">/</span><span>', ['</span><span style="color:#a3be8c;">as</span><span>' => '</span><span style="color:#a3be8c;">search</span><span>', '</span><span style="color:#a3be8c;">uses</span><span>' => </span><span style="color:#b48ead;">function</span><span>() {
</span><span>
</span><span> </span><span style="color:#65737e;">// Check if user has sent a search query
</span><span> </span><span style="color:#b48ead;">if</span><span>($</span><span style="color:#bf616a;">query </span><span>= </span><span style="color:#ebcb8b;">Input</span><span>::</span><span style="color:#bf616a;">get</span><span>('</span><span style="color:#a3be8c;">query</span><span>', </span><span style="color:#d08770;">false</span><span>)) {
</span><span> </span><span style="color:#65737e;">// Use the Elasticquent search method to search ElasticSearch
</span><span> $</span><span style="color:#bf616a;">posts </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>::</span><span style="color:#bf616a;">search</span><span>($</span><span style="color:#bf616a;">query</span><span>);
</span><span> } </span><span style="color:#b48ead;">else </span><span>{
</span><span> </span><span style="color:#65737e;">// Show all posts if no query is set
</span><span> $</span><span style="color:#bf616a;">posts </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>::</span><span style="color:#bf616a;">all</span><span>();
</span><span> }
</span><span>
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#ebcb8b;">View</span><span>::</span><span style="color:#bf616a;">make</span><span>('</span><span style="color:#a3be8c;">home</span><span>', </span><span style="color:#96b5b4;">compact</span><span>('</span><span style="color:#a3be8c;">posts</span><span>'));
</span><span>
</span><span>}]);
</span></code></pre>
<p>Make a template in <code>app/views/home.blade.php</code></p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">html</span><span>>
</span><span><</span><span style="color:#bf616a;">body</span><span>>
</span><span>{{ Form::open(['method' => 'get', 'route' => 'search']) }}
</span><span>
</span><span> {{ Form::input('search', 'query', Input::get('query', ''))}}
</span><span> {{ Form::submit('Filter results') }}
</span><span>
</span><span>{{ Form:: close() }}
</span><span>
</span><span>@foreach($posts as $post)
</span><span> <</span><span style="color:#bf616a;">div</span><span>>
</span><span> <</span><span style="color:#bf616a;">h2</span><span>>{{{ $post->title }}}</</span><span style="color:#bf616a;">h2</span><span>>
</span><span> <</span><span style="color:#bf616a;">div</span><span>>{{{ $post->content }}}</</span><span style="color:#bf616a;">div</span><span>>
</span><span> <</span><span style="color:#bf616a;">div</span><span>><</span><span style="color:#bf616a;">small</span><span>>{{{ $post->tags }}}</</span><span style="color:#bf616a;">small</span><span>></</span><span style="color:#bf616a;">div</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span>@endforeach
</span><span></</span><span style="color:#bf616a;">body</span><span>>
</span><span></</span><span style="color:#bf616a;">html</span><span>>
</span></code></pre>
<p>In the above snippet we create a form that allows us to type in a search term. Below the form we iterate either through all of the posts or all of the search results depending on whether the user has entered a search term.</p>
<p>Here's how it looks currently.</p>
<p><img src="/media/elastic-search-results.png" alt="ElasticSearch Results" /></p>
<p>We could stop now and the search would work fairly well. But where is the fun in that? Let's tinker and see how we can improve our search results.</p>
<h2 id="fine-tuning-your-search">Fine-tuning your search</h2>
<p>Elasticquent has another method called <code>searchByQuery()</code> which will allow us to specify more details on how we want ElasticSearch to query our data. Here's an example (taken and modified from the Elasticquent docs)</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php
</span><span>$</span><span style="color:#bf616a;">posts </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>::</span><span style="color:#bf616a;">searchByQuery</span><span>(['</span><span style="color:#a3be8c;">match</span><span>' => ['</span><span style="color:#a3be8c;">title</span><span>' => </span><span style="color:#ebcb8b;">Input</span><span>::</span><span style="color:#bf616a;">get</span><span>('</span><span style="color:#a3be8c;">query</span><span>', '')]]);
</span></code></pre>
<p>In the above example only the title is searched. How does this differ from the <code>search()</code> method behind the scenes? The <code>search()</code> query will match all parameters including our content and tags fields.</p>
<p>If we try searching our data now with text from the <code>content</code> field you will notice drastically different results. We may even notice different results when you take data from the title fields, too. This is because ElasticSearch generates a score from the data it searches. Any relevant text in the queried fields will improve that score.</p>
<p>Let's give our <code>title</code> priority so that searches that match our titles will appear above those that only appear in the content.</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php
</span><span>$</span><span style="color:#bf616a;">posts </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>::</span><span style="color:#bf616a;">searchByQuery</span><span>([
</span><span> '</span><span style="color:#a3be8c;">multi_match</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">query</span><span>' => </span><span style="color:#ebcb8b;">Input</span><span>::</span><span style="color:#bf616a;">get</span><span>('</span><span style="color:#a3be8c;">query</span><span>', ''),
</span><span> '</span><span style="color:#a3be8c;">fields</span><span>' => [ "</span><span style="color:#a3be8c;">title^5</span><span>", "</span><span style="color:#a3be8c;">content</span><span>"]
</span><span> ],
</span><span>]);
</span></code></pre>
<p>The caret symbol (^) lets ElasticSearch know we want the title field to have added weight to it by the number that follows it.</p>
<p>That's all well and good, but now we want to search our tags because they have specific keywords and phrases we want to match in the search results.</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php
</span><span>$</span><span style="color:#bf616a;">posts </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>::</span><span style="color:#bf616a;">searchByQuery</span><span>([
</span><span> '</span><span style="color:#a3be8c;">match_phrase</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">tags</span><span>' => </span><span style="color:#ebcb8b;">Input</span><span>::</span><span style="color:#bf616a;">get</span><span>('</span><span style="color:#a3be8c;">query</span><span>', '')
</span><span> ]
</span><span>]);
</span></code></pre>
<p>To make use of both searches we need to do a compound query. There are many types of compound query but the one we'll use is the <code>bool</code> query.</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php
</span><span>$</span><span style="color:#bf616a;">posts </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>::</span><span style="color:#bf616a;">searchByQuery</span><span>([
</span><span> "</span><span style="color:#a3be8c;">bool</span><span>" => [
</span><span> '</span><span style="color:#a3be8c;">must</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">multi_match</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">query</span><span>' => </span><span style="color:#ebcb8b;">Input</span><span>::</span><span style="color:#bf616a;">get</span><span>('</span><span style="color:#a3be8c;">query</span><span>', ''),
</span><span> '</span><span style="color:#a3be8c;">fields</span><span>' => [ "</span><span style="color:#a3be8c;">title^2</span><span>", "</span><span style="color:#a3be8c;">content</span><span>"]
</span><span> ],
</span><span> ],
</span><span> "</span><span style="color:#a3be8c;">should</span><span>" => [
</span><span> '</span><span style="color:#a3be8c;">match</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">tags</span><span>' => [
</span><span> "</span><span style="color:#a3be8c;">query</span><span>" => </span><span style="color:#ebcb8b;">Input</span><span>::</span><span style="color:#bf616a;">get</span><span>('</span><span style="color:#a3be8c;">query</span><span>', ''),
</span><span> "</span><span style="color:#a3be8c;">type</span><span>" => "</span><span style="color:#a3be8c;">phrase</span><span>"
</span><span> ]
</span><span> ]
</span><span> ]
</span><span> ]
</span><span>]);
</span></code></pre>
<p>In a <code>bool</code> query we can specify three parameters: <code>must</code>, <code>should</code> and <code>must_not</code>. In ours we have specified we must get a match from the title or content field and that we can optionally also get a match from the tags field.</p>
<p>We can also completely filter out specific terms if they are irrelevent with a filter. Here we're using the <a href="http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-not-filter.html">not_filter</a>. You can read more on filters <a href="http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-filters.html">here</a></p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php
</span><span>$</span><span style="color:#bf616a;">posts </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>::</span><span style="color:#bf616a;">searchByQuery</span><span>([
</span><span> '</span><span style="color:#a3be8c;">filtered</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">filter</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">not</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">terms</span><span>' => ['</span><span style="color:#a3be8c;">title</span><span>' => ['</span><span style="color:#a3be8c;">impedit</span><span>', '</span><span style="color:#a3be8c;">voluptatem</span><span>']]
</span><span> ]
</span><span> ],
</span><span> '</span><span style="color:#a3be8c;">query</span><span>' => [
</span><span> "</span><span style="color:#a3be8c;">bool</span><span>" => [
</span><span> '</span><span style="color:#a3be8c;">must</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">multi_match</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">query</span><span>' => </span><span style="color:#ebcb8b;">Input</span><span>::</span><span style="color:#bf616a;">get</span><span>('</span><span style="color:#a3be8c;">query</span><span>', ''),
</span><span> '</span><span style="color:#a3be8c;">fields</span><span>' => [ "</span><span style="color:#a3be8c;">title^2</span><span>", "</span><span style="color:#a3be8c;">content</span><span>"]
</span><span> ],
</span><span> ],
</span><span> "</span><span style="color:#a3be8c;">should</span><span>" => [
</span><span> '</span><span style="color:#a3be8c;">match</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">tags</span><span>' => [
</span><span> "</span><span style="color:#a3be8c;">query</span><span>" => </span><span style="color:#ebcb8b;">Input</span><span>::</span><span style="color:#bf616a;">get</span><span>('</span><span style="color:#a3be8c;">query</span><span>', ''),
</span><span> "</span><span style="color:#a3be8c;">type</span><span>" => "</span><span style="color:#a3be8c;">phrase</span><span>"
</span><span> ]
</span><span> ]
</span><span> ]
</span><span> ]
</span><span> ],
</span><span> ],
</span><span>]);
</span></code></pre>
<p>Between lines 2-7 we're specifying that when 'impedit' or 'voluptatem' are not in the title. </p>
<p>If we had a <code>published</code> field in out database another useful filter would be to only search published posts.</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php
</span><span>$</span><span style="color:#bf616a;">pages </span><span>= $</span><span style="color:#bf616a;">this</span><span>-></span><span style="color:#bf616a;">page</span><span>-></span><span style="color:#bf616a;">searchByQuery</span><span>([
</span><span> '</span><span style="color:#a3be8c;">filtered</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">filter</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">term</span><span>' => ['</span><span style="color:#a3be8c;">published</span><span>' => '</span><span style="color:#a3be8c;">1</span><span>']
</span><span> ],
</span><span> '</span><span style="color:#a3be8c;">query</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">multi_match</span><span>' => [
</span><span> '</span><span style="color:#a3be8c;">query</span><span>' => </span><span style="color:#ebcb8b;">Input</span><span>::</span><span style="color:#bf616a;">get</span><span>('</span><span style="color:#a3be8c;">query</span><span>', ''),
</span><span> '</span><span style="color:#a3be8c;">fields</span><span>' => [ "</span><span style="color:#a3be8c;">title^2</span><span>", "</span><span style="color:#a3be8c;">content</span><span>"]
</span><span> ],
</span><span> ],
</span><span> ],
</span><span>]);
</span></code></pre>
<h2 id="summary">Summary</h2>
<p>That's it for our search. We've looked at setting up Elasticquent with our model and looked several ways we can customise our search results. </p>
<p>We can use queries to order our search results by score, we can create compound queries for more complex search results and filters for simple boolean queries.</p>
<p>Although Elasticquent is great for a basic search engine there's also the official <a href="https://github.com/elasticsearch/elasticsearch-php">ElasticSearch client for PHP</a> for when you need something more advanced such as fragment highlighting or autocomplete. </p>
<p>I'm really enjoying what I've learned so far with ElasticSearch and I'm very glad that I decided to pick it up. I also really recommend <a href="http://www.amazon.co.uk/gp/product/B00JXLF7AK/ref=as_li_tl?ie=UTF8&camp=1634&creative=19450&creativeASIN=B00JXLF7AK&linkCode=as2&tag=fullstan-21&linkId=3UGJUFM7O7NQS4GJ">ElasticSearch Server - Second Edition</a> (Amazon referral link). I'm about 40% of the way through and I've learned a lot already.</p>
<img src="https://ir-uk.amazon-adsystem.com/e/ir?t=fullstan-21&l=as2&o=2&a=B00JXLF7AK" width="1" height="1">