Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Filter pacticipant name on index page #446

Merged
merged 1 commit into from
Jul 6, 2021

Conversation

bangn
Copy link
Contributor

@bangn bangn commented Jun 4, 2021

This is the look and feel
image

div.top-of-group {
margin-top: 20px;
margin-bottom: 20px;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I test it locally, looks like the change in the index.css file does not
get reflected
Copy those changes and put into Firefox css style editor result in the image in
the PR description

Im not sure what I did wrong which made the index.css changes not get copied
to the server asset. Any hint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I can't think of anything to be honest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is fixed now. Im not sure why it did not work before 🤷

end
end

context "when it is NOT blank and exist" do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer not having to many level of indentation, hence the and in the
description. Please let me know if you think otherwise

@bangn bangn force-pushed the issue-318 branch 3 times, most recently from a14a146 to d103459 Compare June 7, 2021 02:07
@bangn
Copy link
Contributor Author

bangn commented Jun 7, 2021

This is to fix #318

@bethesque
Copy link
Member

Can we re-think the search form? How about a single field where you can type in anything, and it does a partial match with either consumer or provider names? I feel like having to type in the exact consumer/provider name might be a bit fiddley.

eg. Search: foo bar would return the "Foo/Bar" pact as well as the "Fooby/Barrel" pact, as well as the "foo-bar/xyz" pact.

This is how the Pactflow search bar works.

What do you think?

@bethesque
Copy link
Member

Like this except with form submission instead of dynamic.

search

@bethesque
Copy link
Member

I did not expect that gif to be so big 😆

@bangn
Copy link
Contributor Author

bangn commented Jun 8, 2021

Can we re-think the search form? How about a single field where you can type in anything, and it does a partial match with either consumer or provider names? I feel like having to type in the exact consumer/provider name might be a bit fiddley.

Yes agree 👍

Search: foo would return the "Foo/Bar" pact as well as the "Fooby/Barrel" pact, as well as the "foo-bar/xyz" pact.

This can be archieved by having some sort of regex matching. However

eg. Search: foo bar would return the "Foo/Bar" pact as well as the "Fooby/Barrel" pact, as well as the "foo-bar/xyz" pact.

This may need some kind of fuzzy search matching. This gem comes to my mind https://github.com/kiyoka/fuzzy-string-match.
It gives a better user experience with a cost of extra dependency.

Im leaning toward using some kind of regex matching instead of adding extra dependency.

What do you think?

@bethesque
Copy link
Member

I think let's just start with a substring, ignore case match and get feedback on that.

@bethesque
Copy link
Member

bethesque commented Jun 9, 2021

You can do a substring, case ignore match using the Sequel gem like so:

# I found out the hard way that underscores are wildcard characters in SQL, so need to be escaped
[7] pry(main)> terms = ['foo','bar'].collect{ | term| term.gsub("_", "\\_") } 
[9] pry(main)> PactBroker::Domain::Pacticipant.where( Sequel.|(* terms.collect{ | term | Sequel.ilike(:name, "%#{term}%")} ))
=> #<Sequel::SQLite::Dataset: "SELECT * FROM `pacticipants` WHERE ((UPPER(`name`) LIKE UPPER('%foo%') ESCAPE '\\') OR (UPPER(`name`) LIKE UPPER('%bar%') ESCAPE '\\'))">

Then find any pact that has either the consumer or provider in that list of pacticipants.

error_messages = []

pacticipant = pacticipant_service.find_pacticipant_by_name(pacticipant_name)
def self.filter_item_by_pacticipant_name(index_items, pacticipant_name)
Copy link
Member

@bethesque bethesque Jun 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The query to generate the index results is quite intensive, and can be slow. It's better to filter at the SQL level, not do the entire query and then filter in memory.

Find the matching pacticipants as per the example that I posted earlier, get their IDs, then update the base query to do something like:

query = query.where( Sequel.|( { consumer_id: pacticipant_ids}, { provider_id: pacticipant_ids} ) )

@bangn bangn force-pushed the issue-318 branch 2 times, most recently from 94ca4e2 to 84dff5e Compare June 11, 2021 20:48
query
end

def self.pacticipant_name_query(pacticipant_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is getting closer! But it's still filtering a really big slow dataset (the entire index query) on a string match, rather than finding the pacticipants (a really small query) on a string match, and then being able to set the exact IDs of the consumer and provider in the big query (which will make it super quick). What's your thinking behind that?

query
end

def self.pacticipant_name_query(pacticipant_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is getting closer! But it's still filtering a really big slow dataset (the entire index query) on a string match, rather than finding the pacticipants (a really small query) on a string match, and then being able to set the exact IDs of the consumer and provider in the big query (which will make it super quick). What's your thinking behind that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's your thinking behind that?

I was thinking doing 2 round trips to the database is a bit inefficient.
However, I think I'm wrong as you said filtering string match with ilike is also an expensive query

I'll do it the way you suggested and probably find the way to optimise the current index query on a different issue/PR :)

Thanks for being patient with me :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attemp in 8fd82a5

The new SQL query looks like below

SELECT pact_publications.*
FROM pact_publications
INNER JOIN pacticipants AS consumers ON (pact_publications.consumer_id = consumers.id)
INNER JOIN pacticipants AS providers ON (pact_publications.provider_id = providers.id)
INNER JOIN versions AS cv ON (pact_publications.consumer_version_id = cv.id)
WHERE ((pact_publications.id IN
          (SELECT p.*
           FROM
             (SELECT pact_publications.id
              FROM pact_publications
              INNER JOIN versions AS cv ON (pact_publications.consumer_version_id = cv.id)
              LEFT JOIN
                (SELECT consumer_id,
                        provider_id,
                        "order"
                 FROM pact_publications
                 INNER JOIN versions AS cv ON (pact_publications.consumer_version_id = cv.id)) AS pp2 ON ((pact_publications.consumer_id = pp2.consumer_id)
                                                                                                          AND (pact_publications.provider_id = pp2.provider_id)
                                                                                                          AND (pp2.order > cv.order))
              WHERE (pp2.order IS NULL)) AS p
           INNER JOIN latest_pact_publication_ids_for_consumer_versions AS lp ON (lp.pact_publication_id = p.id)))
       AND (pact_publications.id IN
              (SELECT pact_publications.id
               FROM pact_publications
               INNER JOIN pacticipants AS consumers ON (pact_publications.consumer_id = consumers.id)
               INNER JOIN pacticipants AS providers ON (pact_publications.provider_id = providers.id)
               WHERE ((providers.name ILIKE '%test%' ESCAPE '\')
                      OR (providers.name ILIKE '%ap\_tesp%' ESCAPE '\')
                      OR (consumers.name ILIKE '%test\_d%' ESCAPE '\')
                      OR (consumers.name ILIKE '%appdf%' ESCAPE '\')))))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please let me know how you think.
There is a failed test in app_spec.rb which Im a bit baffled to understand. Will look at it later after you are happy with the filtering :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the part that gets added to a big index query

AND (pact_publications.id IN
              (SELECT pact_publications.id
               FROM pact_publications
               INNER JOIN pacticipants AS consumers ON (pact_publications.consumer_id = consumers.id)
               INNER JOIN pacticipants AS providers ON (pact_publications.provider_id = providers.id)
               WHERE ((providers.name ILIKE '%test%' ESCAPE '\')
                      OR (providers.name ILIKE '%ap\_tesp%' ESCAPE '\')
                      OR (consumers.name ILIKE '%test\_d%' ESCAPE '\')
                      OR (consumers.name ILIKE '%appdf%' ESCAPE '\')))))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with this approach is that it has to do all the joins, creating a big dataset, then remove lines by filtering on the pacticipant names afterwards.

By doing the query to get the pacticipant ids first, and only joining those consumers, then the dataset never gets big in the first place. Ideally, we want a query that looks like:

SELECT pact_publications.id
               FROM pact_publications
               INNER JOIN pacticipants AS consumers ON (pact_publications.consumer_id = consumers.id and pact_publications.consumer_id in [<the ids from the first query>])
               INNER JOIN pacticipants AS providers ON (pact_publications.provider_id = providers.id and providers.id in [<the ids from the first query>])

I've learned that making an extra round trip to the database is worth the trade off when you can significantly cut down on the amount of data being processed in a subsequent query.

Copy link
Member

@bethesque bethesque Jun 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I put those IN statements in the wrong part of the query.

SELECT pact_publications.*
FROM pact_publications
INNER JOIN pacticipants AS consumers ON (pact_publications.consumer_id = consumers.id)
INNER JOIN pacticipants AS providers ON (pact_publications.provider_id = providers.id)
INNER JOIN versions AS cv ON (pact_publications.consumer_version_id = cv.id)
WHERE ((pact_publications.id IN
          (SELECT p.*
           FROM
             (SELECT pact_publications.id
              FROM pact_publications
--- filter here
WHERE ( pact_publications.consumer_id IN [...]  OR pact_publications.provider_id IN [...])
              INNER JOIN versions AS cv ON (pact_publications.consumer_version_id = cv.id)
              LEFT JOIN
                (SELECT consumer_id,
                        provider_id,
                        "order"
                 FROM pact_publications
                 INNER JOIN versions AS cv ON (pact_publications.consumer_version_id = cv.id)) AS pp2 ON ((pact_publications.consumer_id = pp2.consumer_id)
                                                                                                          AND (pact_publications.provider_id = pp2.provider_id)
                                                                                                          AND (pp2.order > cv.order))
              WHERE (pp2.order IS NULL)) AS p
           INNER JOIN latest_pact_publication_ids_for_consumer_versions AS lp ON (lp.pact_publication_id = p.id)))
       AND (pact_publications.id IN
              (SELECT pact_publications.id
               FROM pact_publications
--- filter here
WHERE ( pact_publications.consumer_id IN [...]  OR pact_publications.provider_id IN [...])
               INNER JOIN pacticipants AS consumers ON (pact_publications.consumer_id = consumers.id)
               INNER JOIN pacticipants AS providers ON (pact_publications.provider_id = providers.id)
)

See how that's filtering the base table before it gets joined, rather than on a joined table after it gets joined?

Copy link
Contributor Author

@bangn bangn Jun 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 80388e9

This is the final SQL. As we already got the pacticipant ids, we can use them in a subquery too. I think it will also boost up the index query.

SELECT pact_publications.*
FROM pact_publications
INNER JOIN pacticipants AS consumers ON (pact_publications.consumer_id = consumers.id)
INNER JOIN pacticipants AS providers ON (pact_publications.provider_id = providers.id)
INNER JOIN versions AS cv ON (pact_publications.consumer_version_id = cv.id)
WHERE (pact_publications.id IN
         (SELECT p.*
          FROM
            (SELECT pact_publications.id
             FROM pact_publications
             INNER JOIN versions AS cv ON (pact_publications.consumer_version_id = cv.id)
             LEFT JOIN
               (SELECT consumer_id,
                       provider_id,
                ORDER
                FROM pact_publications
                INNER JOIN versions AS cv ON (pact_publications.consumer_version_id = cv.id)
--- use pacticticpant ids in subquery here
                WHERE (pact_publications.id IN (1,
                                                2,
                                                3,
                                                4))) AS pp2 ON ((pact_publications.consumer_id = pp2.consumer_id)
                                                                AND (pact_publications.provider_id = pp2.provider_id)
                                                                AND (pp2.order > cv.order))
--- filter by pacticipant ids here
             WHERE ((pact_publications.id IN (1,
                                              2,
                                              3,
                                              4))
                    AND (pp2.order IS NULL))) AS p
          INNER JOIN latest_pact_publication_ids_for_consumer_versions AS lp ON (lp.pact_publication_id = p.id)));

@bangn bangn force-pushed the issue-318 branch 2 times, most recently from 07bf41c to 6a7345d Compare June 17, 2021 12:30
@bangn
Copy link
Contributor Author

bangn commented Jun 17, 2021

sqlite action failed due to
image

Im not sure why it is failing

@bangn bangn force-pushed the issue-318 branch 2 times, most recently from 313d324 to 80388e9 Compare June 17, 2021 13:03
@bethesque
Copy link
Member

It's failing because the pull request build doesn't have access to the env var it needs. However, I thought I toggled that to only run when it had the env var. I'll have a look at it.

Sequel.|( *terms.map { |term| Sequel.ilike(Sequel[:consumers][:name], "%#{term}%") })
)

PactBroker::Pacts::PactPublication
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still confused as to why you're querying the pact publication table, instead of just querying the pacticipant table directly. To do this query requires that you join the pacticipant table twice to the pact publication table, and then filter on the entire dataset based on two columns (there could be 1000s of pact publications).

If you query the pacticipant table directly, you only do the filtering once, there are no joins, and it's there are likely to be 10s of pacticipants maximum.

I feel like we're not on the same page here. Is there something I'm missing? Shall we have a lunch time catch up?

@bethesque
Copy link
Member

I've updated the code that runs the pact verify to check for empty string as well as nil. Merge master in, and see if that fixes the Sqlite build.

@bangn bangn force-pushed the issue-318 branch 3 times, most recently from 2538ec1 to d48df4f Compare June 21, 2021 14:03
@bethesque
Copy link
Member

Just doing some local testing. When I put in "bar" it doesn't find the "bar-provider"

Screen Shot 2021-06-22 at 8 57 53 am

Screen Shot 2021-06-22 at 8 59 00 am

Screen Shot 2021-06-22 at 8 58 04 am

@bangn
Copy link
Contributor Author

bangn commented Jun 22, 2021

Oh I have not tokenise by spliting - 🤦
Probably split by -, / and :space:?

@bethesque
Copy link
Member

There was no dash in the search string. I just put in "bar". "bar" should match "bar-provider".

@bethesque
Copy link
Member

The unit test for search indicates that it should work, so I'm guessing the issue is somewhere outside that.

@bangn
Copy link
Contributor Author

bangn commented Jun 22, 2021

Somehow this query

          base.where(Sequel.|(
            Sequel[:pact_publications][:consumer_id] => pacticipant_ids,
            Sequel[:pact_publications][:provider_id] => pacticipant_ids
          ))

return an AND operator instead of an OR

SELECT `pact_publications`.`id` FROM `pact_publications`
WHERE ((`pact_publications`.`consumer_id` IN (1, 3)) AND (`pact_publications`.`provider_id` IN (1, 3)))

I was expecting Sequel.| should return an OR as in https://sequel.jeremyevans.net/rdoc/files/doc/querying_rdoc.html#label-SQL-3A-3AExpression

Sequel.or works tho. Am I missing something here?

@bangn
Copy link
Contributor Author

bangn commented Jun 22, 2021

Found the issue
This code works

          base.where(Sequel.|(
            { Sequel[:pact_publications][:consumer_id] => pacticipant_ids },
            { Sequel[:pact_publications][:provider_id] => pacticipant_ids }
          ))

@bangn
Copy link
Contributor Author

bangn commented Jun 22, 2021

Fixed in c45f3e4

@bethesque
Copy link
Member

🎉 Awesome! The search results are showing the expected results now.

Next steps:

  • Keep the search text displayed in the Pacticipant name box after submission (it disappears)
  • Allow the user to clear the search and view all pacticipants again.
  • Add the search form to the "tags" view.

@bangn
Copy link
Contributor Author

bangn commented Jun 24, 2021

Next steps:
Keep the search text displayed in the Pacticipant name box after submission (it disappears)
Allow the user to clear the search and view all pacticipants again.
Add the search form to the "tags" view.

Keep the search text displayed in the Pacticipant name box after submission (it disappears)

Done

I'll work on other items and will keep you posted :)

@bangn
Copy link
Contributor Author

bangn commented Jun 29, 2021

Allow the user to clear the search and view all pacticipants again.
Add the search form to the "tags" view.

Done

Used jQuery to redirect to the correct page in this case. I feel a bit hacky tho. Please let me know if you have a better idea

@bangn
Copy link
Contributor Author

bangn commented Jul 5, 2021

Hi @bethesque do you have any feedbacks on this? :D

@bethesque
Copy link
Member

bethesque commented Jul 5, 2021

Almost there!

  • Let's rename "Pacticipant name" to "Search", because it's not just one pacticipant name, it searches across both consumer and provider.
  • Can you move the search and reset buttons to the right of the text box, so it doesn't take up so much vertical space.
  • When I go to the tags index page, and then put in search text and press enter, it takes me back to the no-tags index page. Can you make the enter key work the same way as pressing the search button.

@bangn
Copy link
Contributor Author

bangn commented Jul 5, 2021

Latest view of the form and functionality :D
Please let me know if you have any feedback

test-with-show-and-show-with-tags.mp4

@bethesque
Copy link
Member

Fantastic! Love it. Thank you so much.

@bethesque bethesque merged commit fd882da into pact-foundation:master Jul 6, 2021
@bangn bangn deleted the issue-318 branch July 12, 2021 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants